diff --git a/build.gradle.kts b/build.gradle.kts index 10a77c4..1525c7c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.mongodb:mongodb-driver-core:5.3.1") + implementation("org.mongodb:mongodb-driver-sync:5.3.1") + developmentOnly("org.springframework.boot:spring-boot-devtools") testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/src/main/java/com/example/demo/Application.java b/src/main/java/com/example/demo/Application.java index b464ed7..a250406 100644 --- a/src/main/java/com/example/demo/Application.java +++ b/src/main/java/com/example/demo/Application.java @@ -1,11 +1,25 @@ package com.example.demo; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } + + @Bean + public MongoClient mongoClient() { + return MongoClients.create("mongodb://localhost:27017"); + } + + @Bean + public MongoDatabase mongoDatabase(MongoClient mongoClient) { + return mongoClient.getDatabase("demo"); + } } diff --git a/src/main/java/com/example/demo/application/CartService.java b/src/main/java/com/example/demo/application/CartService.java new file mode 100644 index 0000000..121579a --- /dev/null +++ b/src/main/java/com/example/demo/application/CartService.java @@ -0,0 +1,61 @@ +package com.example.demo.application; + +import com.example.demo.infrastructure.LineItemDAO; +import com.example.demo.infrastructure.ProductDAO; +import com.example.demo.model.Cart; +import com.example.demo.model.LineItem; +import com.example.demo.model.Product; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class CartService { + private final LineItemDAO lineItemDAO; + private final ProductDAO productDAO; + + public CartService(LineItemDAO lineItemDAO, ProductDAO productDAO) { + this.lineItemDAO = lineItemDAO; + this.productDAO = productDAO; + } + + public Cart getCart() { + List lineItems = lineItemDAO.findAll(); + + lineItems.forEach(lineItem -> { + String productId = lineItem.getProductId(); + Product product = productDAO.find(productId); + + int unitPrice = product.getPrice(); + int quantity = lineItem.getQuantity(); + + lineItem.setProductName(product.getName()); + lineItem.setUnitPrice(product.getPrice()); + lineItem.setTotalPrice(unitPrice * quantity); + }); + + int totalPrice = lineItems.stream() + .mapToInt(LineItem::getTotalPrice) + .sum(); + + return new Cart(lineItems, totalPrice); + } + + public void addProduct(String productId, int quantity) { + + List lineItems = lineItemDAO.findAll(); + + LineItem lineItem = lineItems.stream() + .filter(i -> i.getProductId().equals(productId)) + .findFirst() + .orElse(null); + + if (lineItem == null) { + lineItem = new LineItem(productId, quantity); + lineItemDAO.add(lineItem); + return; + } + lineItem.setQuantity(lineItem.getQuantity() + quantity); + lineItemDAO.update(lineItem); + } +} diff --git a/src/main/java/com/example/demo/controllers/CartController.java b/src/main/java/com/example/demo/controllers/CartController.java index 0e1469a..0c0d5b3 100644 --- a/src/main/java/com/example/demo/controllers/CartController.java +++ b/src/main/java/com/example/demo/controllers/CartController.java @@ -1,5 +1,10 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; +import com.example.demo.controllers.dtos.CartDto; +import com.example.demo.model.Cart; +import com.example.demo.model.LineItem; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -7,8 +12,31 @@ @RestController @RequestMapping("/cart") public class CartController { + + private final CartService cartService; + + public CartController(CartService cartService) { + this.cartService = cartService; + } + @GetMapping - String detail() { - return ""; + CartDto detail() { + + Cart cart = cartService.getCart(); + return new CartDto( + cart.getLineItems().stream() + .map(this::mapToDto) + .toList(), + cart.getTotalPrice()); + } + + private CartDto.LineItemDto mapToDto(LineItem lineItem) { + return new CartDto.LineItemDto( + lineItem.getId(), + lineItem.getProductId(), + lineItem.getProductName(), + lineItem.getUnitPrice(), + lineItem.getQuantity(), + lineItem.getTotalPrice()); } } diff --git a/src/main/java/com/example/demo/controllers/LineItemController.java b/src/main/java/com/example/demo/controllers/LineItemController.java index 20dc170..8540162 100644 --- a/src/main/java/com/example/demo/controllers/LineItemController.java +++ b/src/main/java/com/example/demo/controllers/LineItemController.java @@ -1,7 +1,11 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; +import com.example.demo.controllers.dtos.AddProductToCartDto; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -9,9 +13,21 @@ @RestController @RequestMapping("/cart/line-items") public class LineItemController { + + private final CartService cartService; + + public LineItemController(CartService cartService) { + this.cartService = cartService; + } + @PostMapping @ResponseStatus(HttpStatus.CREATED) - void create() { - // + void create( + @Valid @RequestBody AddProductToCartDto addProductToCartDto + ) { + cartService.addProduct( + addProductToCartDto.productId(), + addProductToCartDto.quantity() + ); } } diff --git a/src/main/java/com/example/demo/controllers/dtos/AddProductToCartDto.java b/src/main/java/com/example/demo/controllers/dtos/AddProductToCartDto.java new file mode 100644 index 0000000..3f8921a --- /dev/null +++ b/src/main/java/com/example/demo/controllers/dtos/AddProductToCartDto.java @@ -0,0 +1,12 @@ +package com.example.demo.controllers.dtos; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +public record AddProductToCartDto( + @NotBlank + String productId, + @Positive + int quantity +) { +} diff --git a/src/main/java/com/example/demo/controllers/dtos/CartDto.java b/src/main/java/com/example/demo/controllers/dtos/CartDto.java new file mode 100644 index 0000000..02a9de5 --- /dev/null +++ b/src/main/java/com/example/demo/controllers/dtos/CartDto.java @@ -0,0 +1,18 @@ +package com.example.demo.controllers.dtos; + +import java.util.List; + +public record CartDto( + List lineItems, + int totalPrice +) { + public record LineItemDto( + String id, + String productId, + String productName, + int unitPrice, + int quantity, + int totalPrice + ) { + } +} diff --git a/src/main/java/com/example/demo/infrastructure/LineItemDAO.java b/src/main/java/com/example/demo/infrastructure/LineItemDAO.java new file mode 100644 index 0000000..3598d2d --- /dev/null +++ b/src/main/java/com/example/demo/infrastructure/LineItemDAO.java @@ -0,0 +1,56 @@ +package com.example.demo.infrastructure; + +import com.example.demo.model.LineItem; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Updates; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class LineItemDAO { + + private final MongoCollection collection; + + public LineItemDAO(MongoDatabase mongoDatabase) { + this.collection = mongoDatabase.getCollection("line_items"); + } + + public List findAll() { + List documents = new ArrayList<>(); + collection.find().into(documents); + + return documents.stream().map(this::mapToModel).toList(); + } + + public void add(LineItem lineItem) { + Document document = new Document() + .append("product_id", lineItem.getProductId()) + .append("quantity", lineItem.getQuantity()); + + collection.insertOne(document); + } + + public void update(LineItem lineItem) { + collection.updateOne( + Filters.eq("_id", new ObjectId(lineItem.getId())), + Updates.combine( + Updates.set("product_id", lineItem.getProductId()), + Updates.set("quantity", lineItem.getQuantity()) + ) + ); + } + + private LineItem mapToModel(Document document) { + return new LineItem( + document.getObjectId("_id").toString(), + document.getString("product_id"), + document.getInteger("quantity") + ); + } +} diff --git a/src/main/java/com/example/demo/infrastructure/ProductDAO.java b/src/main/java/com/example/demo/infrastructure/ProductDAO.java new file mode 100644 index 0000000..e8e7c5c --- /dev/null +++ b/src/main/java/com/example/demo/infrastructure/ProductDAO.java @@ -0,0 +1,37 @@ +package com.example.demo.infrastructure; + +import com.example.demo.model.LineItem; +import com.example.demo.model.Product; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ProductDAO { + + private final MongoDatabase mongoDatabase; + + public ProductDAO(MongoDatabase mongoDatabase) { + this.mongoDatabase = mongoDatabase; + } + + public Product find(String productId) { + + MongoCollection collection = mongoDatabase.getCollection("products"); + + Document document = collection.find( + Filters.eq("_id", new ObjectId(productId)) + ).first(); + + return new Product( + document.getObjectId("_id").toString(), + document.getString("name"), + document.getInteger("price") + ); + } +} diff --git a/src/main/java/com/example/demo/model/Cart.java b/src/main/java/com/example/demo/model/Cart.java new file mode 100644 index 0000000..913a979 --- /dev/null +++ b/src/main/java/com/example/demo/model/Cart.java @@ -0,0 +1,23 @@ +package com.example.demo.model; + +import java.util.Collections; +import java.util.List; + +public class Cart { + private List lineItems; + private int totalPrice; + + public Cart(List lineItems, int totalPrice) { + this.lineItems = lineItems; + this.totalPrice = totalPrice; + } + + public List getLineItems() { + return Collections.unmodifiableList(lineItems); + } + + public int getTotalPrice() { + return totalPrice; + } + +} diff --git a/src/main/java/com/example/demo/model/LineItem.java b/src/main/java/com/example/demo/model/LineItem.java new file mode 100644 index 0000000..087ea1b --- /dev/null +++ b/src/main/java/com/example/demo/model/LineItem.java @@ -0,0 +1,63 @@ +package com.example.demo.model; + +public class LineItem { + + private String id; + private String productId; + private int quantity; + + private String productName; + private int unitPrice; + private int totalPrice; + + public LineItem(String id, String productId, int quantity) { + this.id = id; + this.productId = productId; + this.quantity = quantity; + } + + public LineItem(String productId, int quantity) { + this.productId = productId; + this.quantity = quantity; + } + + public String getId() { + return id; + } + + public String getProductId() { + return productId; + } + + public int getQuantity() { + return quantity; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public int getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(int unitPrice) { + this.unitPrice = unitPrice; + } + + public int getTotalPrice() { + return totalPrice; + } + + public void setTotalPrice(int totalPrice) { + this.totalPrice = totalPrice; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } +} diff --git a/src/main/java/com/example/demo/model/Product.java b/src/main/java/com/example/demo/model/Product.java new file mode 100644 index 0000000..45f51e7 --- /dev/null +++ b/src/main/java/com/example/demo/model/Product.java @@ -0,0 +1,26 @@ +package com.example.demo.model; + +public class Product { + + private String id; + private String name; + private int price; + + public Product(String id, String name, int price) { + this.id = id; + this.name = name; + this.price = price; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } +} diff --git a/src/test/java/com/example/demo/MongoTest.java b/src/test/java/com/example/demo/MongoTest.java new file mode 100644 index 0000000..96d52c6 --- /dev/null +++ b/src/test/java/com/example/demo/MongoTest.java @@ -0,0 +1,30 @@ +package com.example.demo; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MongoTest { + + @Test + void test() { + + MongoClient mongoClient = MongoClients.create("mongodb://localhost:27017"); + MongoDatabase database = mongoClient.getDatabase("demo"); + MongoCollection collection = database.getCollection("products"); + + List documents = new ArrayList<>(); + collection.find().into(documents); + + assertThat(documents.get(0).getString("name")).isEqualTo("티셔츠"); + assertThat(documents.get(1).getString("name")).isEqualTo("청바지"); + } +} diff --git a/src/test/java/com/example/demo/application/CartServiceTest.java b/src/test/java/com/example/demo/application/CartServiceTest.java new file mode 100644 index 0000000..a5061d4 --- /dev/null +++ b/src/test/java/com/example/demo/application/CartServiceTest.java @@ -0,0 +1,136 @@ +package com.example.demo.application; + +import com.example.demo.infrastructure.LineItemDAO; +import com.example.demo.infrastructure.ProductDAO; +import com.example.demo.model.Cart; +import com.example.demo.model.LineItem; +import com.example.demo.model.Product; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class CartServiceTest { + + private Product product1; + private Product product2; + + private List lineItems; + + private LineItemDAO lineItemDAO; + private ProductDAO productDAO; + private CartService cartService; + + @BeforeEach + void setUp() { + lineItemDAO = mock(LineItemDAO.class); + productDAO = mock(ProductDAO.class); + cartService = new CartService(lineItemDAO, productDAO); + + lineItems = new ArrayList<>(); + given(lineItemDAO.findAll()).willReturn(lineItems); + + product1 = new Product("product-1", "product #1", 5000); + product2 = new Product("product-2", "product #2", 3000); + given(productDAO.find(product1.getId())).willReturn(product1); + given(productDAO.find(product2.getId())).willReturn(product2); + } + + @Test + @DisplayName("장바구니가 비어있으면 총가격은 0원") + void totalPriceIsZero() { + given(lineItemDAO.findAll()).willReturn(List.of()); + + Cart cart = cartService.getCart(); + + assertThat(cart.getTotalPrice()).isEqualTo(0); + } + + @Test + @DisplayName("장바구니에 있는 하나의 상품의 가격을 모두 더해서 총 가격을 구한다.") + void calculateTotalPriceWithOneLineItem() { + int quantity = 2; + addProductInCart(product1, quantity); + + Cart cart = cartService.getCart(); + + assertThat(cart.getTotalPrice()).isEqualTo(product1.getPrice() * quantity); + } + + @Test + @DisplayName("장바구니에 있는 여러 상품의 가격을 모두 더해서 총 가격을 구한다.") + void calculateTotalPriceWithManyLineItems() { + int quantity1 = 2; + int quantity2 = 3; + addProductInCart(product1, quantity1); + addProductInCart(product2, quantity2); + + Cart cart = cartService.getCart(); + + assertThat(cart.getTotalPrice()).isEqualTo(product1.getPrice() * quantity1 + + product2.getPrice() * quantity2); + } + + // 상품담기 + + @Test + @DisplayName("비어있는 장바구니에 상품 담기") + void addProduct(){ + String productId = product1.getId(); + int quantity = 1; + + cartService.addProduct(productId, quantity); + + verify(lineItemDAO).add(argThat(lineItem -> + lineItem.getProductId().equals(productId) + && lineItem.getQuantity() == quantity)); + } + + @Test + @DisplayName("장바구니에 없는 상품 담기") + void addNewProduct(){ + String productId = product1.getId(); + int quantity = 1; + + lineItems.add(new LineItem(product2.getId(), 5)); + + cartService.addProduct(productId, quantity); + + verify(lineItemDAO).add(argThat(lineItem -> + lineItem.getProductId().equals(productId) + && lineItem.getQuantity() == quantity)); + } + + @Test + @DisplayName("장바구니에 이미있는 상품 담기") + void addExistingProduct(){ + String productId = product1.getId(); + int oldQuantity = 1; + int newQuantity = 2; + + lineItems.add(new LineItem(product1.getId(), oldQuantity)); + + cartService.addProduct(productId, newQuantity); + + verify(lineItemDAO).update(argThat(lineItem -> + lineItem.getProductId().equals(productId) + && lineItem.getQuantity() == oldQuantity + newQuantity)); + } + + + + private void addProductInCart(Product product, int quantity) { + String id = "item-"+(lineItems.size() +1); + LineItem lineItem = new LineItem(id, product.getId(), quantity); + lineItems.add(lineItem); + } +} diff --git a/src/test/java/com/example/demo/controllers/CartControllerTest.java b/src/test/java/com/example/demo/controllers/CartControllerTest.java index 619307d..03ac0c4 100644 --- a/src/test/java/com/example/demo/controllers/CartControllerTest.java +++ b/src/test/java/com/example/demo/controllers/CartControllerTest.java @@ -1,12 +1,21 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; +import com.example.demo.model.Cart; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(CartController.class) @@ -14,10 +23,20 @@ class CartControllerTest { @Autowired private MockMvc mockMvc; + @MockBean + private CartService cartService; + @Test @DisplayName("GET /cart") void detail() throws Exception { + + Cart cart = new Cart(List.of(), 0); + given(cartService.getCart()).willReturn(cart); + mockMvc.perform(get("/cart")) + .andExpect(content().string( + containsString("lineItems") + )) .andExpect(status().isOk()); } } diff --git a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java index 41eabbc..7be2ecf 100644 --- a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java +++ b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java @@ -1,9 +1,11 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -15,6 +17,9 @@ class LineItemControllerTest { @Autowired private MockMvc mockMvc; + @MockBean + private CartService cartService; + @Test @DisplayName("POST /cart/line-items") void addProduct() throws Exception { @@ -30,4 +35,36 @@ void addProduct() throws Exception { .content(json)) .andExpect(status().isCreated()); } + + @Test + @DisplayName("POST /cart/line-items - ProductId가 없을 때") + void addProductWithoutProductId() throws Exception { + String json = """ + { + "productId": "", + "quantity": 2 + } + """; + + mockMvc.perform(post("/cart/line-items") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /cart/line-items - 수량이 양수가 아닐때") + void addProductWithInvalidQuantity() throws Exception { + String json = """ + { + "productId": "product-1", + "quantity": 0 + } + """; + + mockMvc.perform(post("/cart/line-items") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } }