diff --git a/src/main/java/kitchenpos/menus/tobe/application/DefaultProfanityChecker.java b/src/main/java/kitchenpos/menus/tobe/application/DefaultProfanityChecker.java new file mode 100644 index 000000000..602db1d2c --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/DefaultProfanityChecker.java @@ -0,0 +1,19 @@ +package kitchenpos.menus.tobe.application; + +import kitchenpos.menus.tobe.domain.ProfanityChecker; +import org.springframework.stereotype.Component; + +@Component("tobeMenuContextProfanityChecker") +public class DefaultProfanityChecker implements ProfanityChecker { + + private final PurgomalumClient purgomalumClient; + + public DefaultProfanityChecker(final PurgomalumClient purgomalumClient) { + this.purgomalumClient = purgomalumClient; + } + + @Override + public boolean containsProfanity(final String text) { + return purgomalumClient.containsProfanity(text); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/MenuCreateRequest.java b/src/main/java/kitchenpos/menus/tobe/application/MenuCreateRequest.java new file mode 100644 index 000000000..39330181f --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/MenuCreateRequest.java @@ -0,0 +1,19 @@ +package kitchenpos.menus.tobe.application; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +public record MenuCreateRequest( + BigDecimal price, + String name, + boolean displayed, + UUID menuGroupId, + List menuProducts +) { + public List productIds() { + return menuProducts.stream() + .map(MenuProductRequest::productId) + .toList(); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/MenuFacade.java b/src/main/java/kitchenpos/menus/tobe/application/MenuFacade.java new file mode 100644 index 000000000..6aac3a1d8 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/MenuFacade.java @@ -0,0 +1,48 @@ +package kitchenpos.menus.tobe.application; + +import kitchenpos.menus.tobe.domain.*; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Component +public class MenuFacade { + private final MenuService menuService; + private final MenuGroupService menuGroupService; + private final ProductClient productClient; + private final ProfanityChecker profanityChecker; + + + public MenuFacade(MenuService menuService, MenuGroupService menuGroupService, ProductClient productClient, ProfanityChecker profanityChecker) { + this.menuService = menuService; + this.menuGroupService = menuGroupService; + this.productClient = productClient; + this.profanityChecker = profanityChecker; + } + + @Transactional + public Menu create(MenuCreateRequest request) { + final MenuName name = new MenuName(request.name(), profanityChecker); + final MenuPrice price = new MenuPrice(request.price()); + final MenuGroup menuGroup = menuGroupService.findById(request.menuGroupId()); + final ProductInfos productInfos = productClient.listByIds(request.productIds()); + final MenuProductCatalog menuProductCatalog = new MenuProductCatalog(request.menuProducts(), productInfos); + return menuService.create(name, price, menuGroup, request.displayed(), menuProductCatalog); + } + + @Transactional + public Menu changePrice(UUID menuId, MenuPriceChangeRequest request) { + final MenuPrice price = new MenuPrice(request.price()); + final Menu menu = menuService.findById(menuId); + final ProductInfos productInfos = productClient.listByIds(menu.findProductIds()); + return menuService.changePrice(menuId, price, productInfos); + } + + @Transactional + public Menu display(UUID menuId) { + final Menu menu = menuService.findById(menuId); + final ProductInfos productInfos = productClient.listByIds(menu.findProductIds()); + return menuService.display(menuId, productInfos); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/MenuGroupCreateRequest.java b/src/main/java/kitchenpos/menus/tobe/application/MenuGroupCreateRequest.java new file mode 100644 index 000000000..f9f24b57e --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/MenuGroupCreateRequest.java @@ -0,0 +1,4 @@ +package kitchenpos.menus.tobe.application; + +public record MenuGroupCreateRequest(String name) { +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/MenuGroupService.java b/src/main/java/kitchenpos/menus/tobe/application/MenuGroupService.java new file mode 100644 index 000000000..9570eed11 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/MenuGroupService.java @@ -0,0 +1,38 @@ +package kitchenpos.menus.tobe.application; + +import kitchenpos.menus.tobe.domain.MenuGroup; +import kitchenpos.menus.tobe.domain.MenuGroupName; +import kitchenpos.menus.tobe.domain.MenuGroupRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +@Service("tobeMenuGroupService") +public class MenuGroupService { + private final MenuGroupRepository menuGroupRepository; + + public MenuGroupService(final MenuGroupRepository menuGroupRepository) { + this.menuGroupRepository = menuGroupRepository; + } + + @Transactional + public MenuGroup create(final MenuGroupCreateRequest request) { + final MenuGroupName name = new MenuGroupName(request.name()); + final MenuGroup menuGroup = new MenuGroup(UUID.randomUUID(), name); + return menuGroupRepository.save(menuGroup); + } + + @Transactional(readOnly = true) + public List findAll() { + return menuGroupRepository.findAll(); + } + + @Transactional(readOnly = true) + public MenuGroup findById(UUID menuGroupId) { + return menuGroupRepository.findById(menuGroupId) + .orElseThrow(NoSuchElementException::new); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/MenuPriceChangeRequest.java b/src/main/java/kitchenpos/menus/tobe/application/MenuPriceChangeRequest.java new file mode 100644 index 000000000..7c2940261 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/MenuPriceChangeRequest.java @@ -0,0 +1,6 @@ +package kitchenpos.menus.tobe.application; + +import java.math.BigDecimal; + +public record MenuPriceChangeRequest(BigDecimal price) { +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/MenuProductRequest.java b/src/main/java/kitchenpos/menus/tobe/application/MenuProductRequest.java new file mode 100644 index 000000000..0783b034a --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/MenuProductRequest.java @@ -0,0 +1,7 @@ +package kitchenpos.menus.tobe.application; + +import java.util.UUID; + +public record MenuProductRequest(UUID productId, long quantity) { + +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/MenuService.java b/src/main/java/kitchenpos/menus/tobe/application/MenuService.java new file mode 100644 index 000000000..23c123245 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/MenuService.java @@ -0,0 +1,60 @@ +package kitchenpos.menus.tobe.application; + +import kitchenpos.menus.tobe.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +@Service("tobeMenuService") +public class MenuService { + + private final MenuRepository menuRepository; + + public MenuService(final MenuRepository menuRepository) { + this.menuRepository = menuRepository; + } + + @Transactional + public Menu create(final MenuName name, final MenuPrice price, final MenuGroup menuGroup, final boolean displayed, final MenuProductCatalog menuProductCatalog) { + final Menu menu = new Menu(UUID.randomUUID(), name, price, menuGroup, displayed, menuProductCatalog); + return menuRepository.save(menu); + } + + @Transactional + public Menu changePrice(final UUID menuId, final MenuPrice price, final ProductInfos productInfos) { + final Menu menu = menuRepository.findById(menuId) + .orElseThrow(NoSuchElementException::new); + menu.changePrice(price, productInfos); + return menu; + } + + @Transactional + public Menu display(final UUID menuId, final ProductInfos productInfos) { + final Menu menu = menuRepository.findById(menuId) + .orElseThrow(NoSuchElementException::new); + menu.display(productInfos); + return menu; + } + + @Transactional + public Menu hide(final UUID menuId) { + final Menu menu = menuRepository.findById(menuId) + .orElseThrow(NoSuchElementException::new); + menu.hide(); + return menu; + } + + @Transactional(readOnly = true) + public List findAll() { + return menuRepository.findAll(); + } + + @Transactional(readOnly = true) + public Menu findById(UUID menuId) { + return menuRepository.findById(menuId) + .orElseThrow(NoSuchElementException::new); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/ProductClient.java b/src/main/java/kitchenpos/menus/tobe/application/ProductClient.java new file mode 100644 index 000000000..efced2063 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/ProductClient.java @@ -0,0 +1,10 @@ +package kitchenpos.menus.tobe.application; + +import kitchenpos.menus.tobe.domain.ProductInfos; + +import java.util.List; +import java.util.UUID; + +public interface ProductClient { + ProductInfos listByIds(List productIds); +} diff --git a/src/main/java/kitchenpos/menus/tobe/application/PurgomalumClient.java b/src/main/java/kitchenpos/menus/tobe/application/PurgomalumClient.java new file mode 100644 index 000000000..71427d106 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/application/PurgomalumClient.java @@ -0,0 +1,6 @@ +package kitchenpos.menus.tobe.application; + +public interface PurgomalumClient { + + boolean containsProfanity(String text); +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/JpaMenuGroupRepository.java b/src/main/java/kitchenpos/menus/tobe/domain/JpaMenuGroupRepository.java new file mode 100644 index 000000000..01bd6fd04 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/JpaMenuGroupRepository.java @@ -0,0 +1,10 @@ +package kitchenpos.menus.tobe.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository("menuGroupToBeRepository") +public interface JpaMenuGroupRepository extends MenuGroupRepository, JpaRepository { +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/JpaMenuRepository.java b/src/main/java/kitchenpos/menus/tobe/domain/JpaMenuRepository.java new file mode 100644 index 000000000..d2cb6df70 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/JpaMenuRepository.java @@ -0,0 +1,16 @@ +package kitchenpos.menus.tobe.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository("menuToBeRepository") +public interface JpaMenuRepository extends MenuRepository, JpaRepository { + @Query("select m from Menu m join m.menuProducts mp where mp.product.id = :productId") + @Override + List findAllByProductId(@Param("productId") UUID productId); +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/Menu.java b/src/main/java/kitchenpos/menus/tobe/domain/Menu.java new file mode 100644 index 000000000..7d0008a66 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/Menu.java @@ -0,0 +1,83 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +@Table(name = "menu") +@Entity(name = "tobeMenu") +public class Menu { + @Column(name = "id", columnDefinition = "binary(16)") + @Id + private UUID id; + + @Embedded + private MenuName name; + + @Embedded + private MenuPrice price; + + @ManyToOne(optional = false) + @JoinColumn( + name = "menu_group_id", + columnDefinition = "binary(16)", + foreignKey = @ForeignKey(name = "fk_menu_to_menu_group") + ) + private MenuGroup menuGroup; + + @Column(name = "displayed", nullable = false) + private boolean displayed; + + @Embedded + private MenuProducts menuProducts; + + @Transient + private UUID menuGroupId; + + protected Menu() { + } + + public Menu(UUID id, MenuName name, MenuPrice price, MenuGroup menuGroup, boolean displayed, MenuProductCatalog menuProductCatalog) { + validatePrice(price, menuProductCatalog.calculateTotalPrice()); + this.id = id; + this.name = name; + this.price = price; + this.menuGroup = menuGroup; + this.displayed = displayed; + this.menuProducts = new MenuProducts(menuProductCatalog); + } + + private void validatePrice(MenuPrice price, BigDecimal total) { + if (!price.isGreaterThan(total)) { + throw new IllegalArgumentException("메뉴 가격이 전체 메뉴 상품의 가격 합보다 같거나 작아야 한다"); + } + } + + public UUID getId() { + return id; + } + + public void changePrice(MenuPrice price, ProductInfos productInfos) { + validatePrice(price, menuProducts.calculateTotalPrice(productInfos)); + this.price = price; + } + + public List findProductIds() { + return menuProducts.getProductIds(); + } + + public void display(ProductInfos productInfos) { + validatePrice(price, menuProducts.calculateTotalPrice(productInfos)); + this.displayed = true; + } + + public void hide() { + this.displayed = false; + } + + public MenuPrice getPrice() { + return price; + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuGroup.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuGroup.java new file mode 100644 index 000000000..2a56205ef --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuGroup.java @@ -0,0 +1,28 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.*; + +import java.util.UUID; + +@Table(name = "menu_group") +@Entity(name = "tobeMenuGroup") +public class MenuGroup { + @Column(name = "id", columnDefinition = "binary(16)") + @Id + private UUID id; + + @Embedded + private MenuGroupName name; + + protected MenuGroup() { + } + + public MenuGroup(final UUID id, final MenuGroupName name) { + this.id = id; + this.name = name; + } + + public UUID getId() { + return id; + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuGroupName.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuGroupName.java new file mode 100644 index 000000000..773ecff55 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuGroupName.java @@ -0,0 +1,25 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public class MenuGroupName { + + @Column(name = "name", nullable = false) + private String name; + + protected MenuGroupName() { + } + + public MenuGroupName(final String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("메뉴 그룹 이름은 필수로 입력해야 합니다."); + } + this.name = name; + } + + public String value() { + return name; + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuGroupRepository.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuGroupRepository.java new file mode 100644 index 000000000..d5e17b533 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuGroupRepository.java @@ -0,0 +1,13 @@ +package kitchenpos.menus.tobe.domain; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface MenuGroupRepository { + MenuGroup save(MenuGroup menuGroup); + + Optional findById(UUID id); + + List findAll(); +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuName.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuName.java new file mode 100644 index 000000000..00ac993cf --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuName.java @@ -0,0 +1,28 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public class MenuName { + + @Column(name = "name", nullable = false) + private String name; + + protected MenuName() { + } + + public MenuName(final String name, final ProfanityChecker profanityChecker) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("메뉴명은 필수로 입력해야 합니다."); + } + if (profanityChecker.containsProfanity(name)) { + throw new IllegalArgumentException("메뉴명에 욕설이 포함되어 있습니다."); + } + this.name = name; + } + + public String value() { + return name; + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuPrice.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuPrice.java new file mode 100644 index 000000000..8c3903793 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuPrice.java @@ -0,0 +1,32 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.math.BigDecimal; +import java.util.Objects; + +@Embeddable +public class MenuPrice { + + @Column(name = "price", nullable = false) + private BigDecimal price; + + protected MenuPrice() { + } + + public MenuPrice(final BigDecimal price) { + if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException(); + } + this.price = price; + } + + public BigDecimal value() { + return price; + } + + public boolean isGreaterThan(BigDecimal sum) { + return price.compareTo(sum) > 0; + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuProduct.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuProduct.java new file mode 100644 index 000000000..c63a1a5a7 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuProduct.java @@ -0,0 +1,37 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.*; + +import java.util.UUID; + +@Table(name = "menu_product") +@Entity(name = "tobeMenuProduct") +public class MenuProduct { + + @Column(name = "seq") + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long seq; + + @Embedded + private MenuProductQuantity quantity; + + @Column(name = "product_id", nullable = false) + private UUID productId; + + protected MenuProduct() { + } + + public MenuProduct(UUID productId, MenuProductQuantity quantity) { + this.productId = productId; + this.quantity = quantity; + } + + public UUID getProductId() { + return productId; + } + + public MenuProductQuantity getQuantity() { + return quantity; + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuProductCatalog.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuProductCatalog.java new file mode 100644 index 000000000..048cdac5b --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuProductCatalog.java @@ -0,0 +1,31 @@ +package kitchenpos.menus.tobe.domain; + +import kitchenpos.menus.tobe.application.MenuProductRequest; + +import java.math.BigDecimal; +import java.util.List; + +public class MenuProductCatalog { + + private final List entries; + + public MenuProductCatalog(List menuProductRequests, ProductInfos productInfos) { + this.entries = menuProductRequests.stream() + .map(menuProductRequest -> new MenuProductInfo( + productInfos.findById(menuProductRequest.productId()).id(), + productInfos.findById(menuProductRequest.productId()).price(), + new MenuProductQuantity(menuProductRequest.quantity()) + )) + .toList(); + } + + public BigDecimal calculateTotalPrice() { + return entries.stream() + .map(menuProductInfo -> menuProductInfo.price().multiply(BigDecimal.valueOf(menuProductInfo.quantity().value()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public List getEntries() { + return entries.stream().toList(); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuProductInfo.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuProductInfo.java new file mode 100644 index 000000000..fc798bd1e --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuProductInfo.java @@ -0,0 +1,7 @@ +package kitchenpos.menus.tobe.domain; + +import java.math.BigDecimal; +import java.util.UUID; + +public record MenuProductInfo(UUID id, BigDecimal price, MenuProductQuantity quantity) { +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuProductQuantity.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuProductQuantity.java new file mode 100644 index 000000000..b3190df5b --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuProductQuantity.java @@ -0,0 +1,25 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public class MenuProductQuantity { + + @Column(name = "quantity", nullable = false) + private long quantity; + + protected MenuProductQuantity() { + } + + public MenuProductQuantity(final long quantity) { + if (quantity < 0) { + throw new IllegalArgumentException("메뉴 상품 수량은 0보다 작을 수 없습니다."); + } + this.quantity = quantity; + } + + public long value() { + return quantity; + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuProducts.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuProducts.java new file mode 100644 index 000000000..b9d657a58 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuProducts.java @@ -0,0 +1,53 @@ +package kitchenpos.menus.tobe.domain; + +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +@Embeddable +public class MenuProducts { + + @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinColumn( + name = "menu_id", + nullable = false, + columnDefinition = "binary(16)", + foreignKey = @ForeignKey(name = "fk_menu_product_to_menu") + ) + private List menuProductList; + + protected MenuProducts() { + } + + public MenuProducts(MenuProductCatalog menuProductCatalog) { + this.menuProductList = menuProductCatalog.getEntries() + .stream() + .map(menuProductInfo -> new MenuProduct(menuProductInfo.id(), menuProductInfo.quantity())) + .toList(); + } + + public List getProductIds() { + return menuProductList.stream() + .map(MenuProduct::getProductId) + .toList(); + } + + public MenuProduct getById(final UUID id) { + return menuProductList.stream() + .filter(menuProduct -> menuProduct.getProductId().equals(id)) + .findFirst() + .orElseThrow(NoSuchElementException::new); + } + + public BigDecimal calculateTotalPrice(ProductInfos productInfos) { + return menuProductList.stream() + .map(menuProduct -> { + ProductInfo productInfo = productInfos.findById(menuProduct.getProductId()); + return productInfo.price().multiply(BigDecimal.valueOf(menuProduct.getQuantity().value())); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/MenuRepository.java b/src/main/java/kitchenpos/menus/tobe/domain/MenuRepository.java new file mode 100644 index 000000000..efdc68783 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/MenuRepository.java @@ -0,0 +1,17 @@ +package kitchenpos.menus.tobe.domain; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface MenuRepository { + Menu save(Menu menu); + + Optional findById(UUID id); + + List findAll(); + + List findAllByIdIn(List ids); + + List findAllByProductId(UUID productId); +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/ProductInfo.java b/src/main/java/kitchenpos/menus/tobe/domain/ProductInfo.java new file mode 100644 index 000000000..aad471800 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/ProductInfo.java @@ -0,0 +1,7 @@ +package kitchenpos.menus.tobe.domain; + +import java.math.BigDecimal; +import java.util.UUID; + +public record ProductInfo(UUID id, BigDecimal price) { +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/ProductInfos.java b/src/main/java/kitchenpos/menus/tobe/domain/ProductInfos.java new file mode 100644 index 000000000..12ff57a4b --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/ProductInfos.java @@ -0,0 +1,47 @@ +package kitchenpos.menus.tobe.domain; + +import kitchenpos.menus.tobe.application.MenuProductRequest; + +import java.math.BigDecimal; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +public record ProductInfos(List products) { + + private static MenuProductRequest getMenuProductRequest(List menuProductRequests, ProductInfo product) { + return menuProductRequests.stream() + .filter(menuProductRequest -> menuProductRequest.productId().equals(product.id())) + .findFirst() + .orElseThrow(NoSuchElementException::new); + } + + public ProductInfo findById(UUID productId) { + return products.stream() + .filter(product -> product.id().equals(productId)) + .findFirst() + .orElseThrow(NoSuchElementException::new); + } + + public int size() { + return products.size(); + } + + public BigDecimal getSumsWithRequests(List menuProductRequests) { + return products.stream() + .map(product -> { + MenuProductRequest menuProduct = getMenuProductRequest(menuProductRequests, product); + return product.price().multiply(BigDecimal.valueOf(menuProduct.quantity())); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public BigDecimal getSums(MenuProducts menuProducts) { + return products.stream() + .map(product -> { + MenuProduct menuProduct = menuProducts.getById(product.id()); + return product.price().multiply(BigDecimal.valueOf(menuProduct.getQuantity().value())); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/domain/ProfanityChecker.java b/src/main/java/kitchenpos/menus/tobe/domain/ProfanityChecker.java new file mode 100644 index 000000000..3dc7cfdf3 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/domain/ProfanityChecker.java @@ -0,0 +1,5 @@ +package kitchenpos.menus.tobe.domain; + +public interface ProfanityChecker { + boolean containsProfanity(String text); +} diff --git a/src/main/java/kitchenpos/menus/tobe/infra/DefaultPurgomalumClient.java b/src/main/java/kitchenpos/menus/tobe/infra/DefaultPurgomalumClient.java new file mode 100644 index 000000000..cf68ba525 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/infra/DefaultPurgomalumClient.java @@ -0,0 +1,27 @@ +package kitchenpos.menus.tobe.infra; + +import kitchenpos.menus.tobe.application.PurgomalumClient; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +@Component("tobeMenuContextPurgomalumClient") +public class DefaultPurgomalumClient implements PurgomalumClient { + private final RestTemplate restTemplate; + + public DefaultPurgomalumClient(final RestTemplateBuilder restTemplateBuilder) { + this.restTemplate = restTemplateBuilder.build(); + } + + @Override + public boolean containsProfanity(final String text) { + final URI url = UriComponentsBuilder.fromUriString("https://www.purgomalum.com/service/containsprofanity") + .queryParam("text", text) + .build() + .toUri(); + return Boolean.parseBoolean(restTemplate.getForObject(url, String.class)); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/infra/RestTemplateConfig.java b/src/main/java/kitchenpos/menus/tobe/infra/RestTemplateConfig.java new file mode 100644 index 000000000..b238bc9f5 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/infra/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package kitchenpos.menus.tobe.infra; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/infra/RestTemplateProductClient.java b/src/main/java/kitchenpos/menus/tobe/infra/RestTemplateProductClient.java new file mode 100644 index 000000000..ccf0442a7 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/infra/RestTemplateProductClient.java @@ -0,0 +1,48 @@ +package kitchenpos.menus.tobe.infra; + +import kitchenpos.menus.tobe.application.ProductClient; +import kitchenpos.menus.tobe.domain.ProductInfo; +import kitchenpos.menus.tobe.domain.ProductInfos; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.UUID; + +@Component +public class RestTemplateProductClient implements ProductClient { + + private static final String BASE_URL = "http://localhost:8080"; + private static final String CHECK_PRODUCTS_EXISTENCE_URL = "/api/tobe/products/ids"; + private final RestTemplate restTemplate; + + public RestTemplateProductClient(final RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public ProductInfos listByIds(List productIds) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + List body = restTemplate.exchange( + BASE_URL + CHECK_PRODUCTS_EXISTENCE_URL, + HttpMethod.POST, + new HttpEntity<>(productIds, headers), + new ParameterizedTypeReference>() { + } + ).getBody(); + return new ProductInfos(body); + + } catch (RestClientException e) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/ui/MenuGroupRestController.java b/src/main/java/kitchenpos/menus/tobe/ui/MenuGroupRestController.java new file mode 100644 index 000000000..dc85b4680 --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/ui/MenuGroupRestController.java @@ -0,0 +1,32 @@ +package kitchenpos.menus.tobe.ui; + +import kitchenpos.menus.tobe.application.MenuGroupCreateRequest; +import kitchenpos.menus.tobe.application.MenuGroupService; +import kitchenpos.menus.tobe.domain.MenuGroup; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.List; + +@RequestMapping("/api/tobe/menu-groups") +@RestController("tobeMenuGroupRestController") +public class MenuGroupRestController { + private final MenuGroupService menuGroupService; + + public MenuGroupRestController(final MenuGroupService menuGroupService) { + this.menuGroupService = menuGroupService; + } + + @PostMapping + public ResponseEntity create(@RequestBody final MenuGroupCreateRequest request) { + final MenuGroup response = menuGroupService.create(request); + return ResponseEntity.created(URI.create("/api/tobe/menu-groups/" + response.getId())) + .body(response); + } + + @GetMapping + public ResponseEntity> findAll() { + return ResponseEntity.ok(menuGroupService.findAll()); + } +} diff --git a/src/main/java/kitchenpos/menus/tobe/ui/MenuRestController.java b/src/main/java/kitchenpos/menus/tobe/ui/MenuRestController.java new file mode 100644 index 000000000..6ac26853b --- /dev/null +++ b/src/main/java/kitchenpos/menus/tobe/ui/MenuRestController.java @@ -0,0 +1,52 @@ +package kitchenpos.menus.tobe.ui; + +import kitchenpos.menus.tobe.application.MenuCreateRequest; +import kitchenpos.menus.tobe.application.MenuFacade; +import kitchenpos.menus.tobe.application.MenuPriceChangeRequest; +import kitchenpos.menus.tobe.application.MenuService; +import kitchenpos.menus.tobe.domain.Menu; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.List; +import java.util.UUID; + +@RequestMapping("/api/tobe/menus") +@RestController("tobeMenuRestController") +public class MenuRestController { + private final MenuService menuService; + private final MenuFacade menuFacade; + + public MenuRestController(final MenuService menuService, final MenuFacade menuFacade) { + this.menuService = menuService; + this.menuFacade = menuFacade; + } + + @PostMapping + public ResponseEntity create(@RequestBody final MenuCreateRequest request) { + final Menu response = menuFacade.create(request); + return ResponseEntity.created(URI.create("/api/tobe/menus/" + response.getId())) + .body(response); + } + + @PutMapping("/{menuId}/price") + public ResponseEntity changePrice(@PathVariable final UUID menuId, @RequestBody final MenuPriceChangeRequest request) { + return ResponseEntity.ok(menuFacade.changePrice(menuId, request)); + } + + @PutMapping("/{menuId}/display") + public ResponseEntity display(@PathVariable final UUID menuId) { + return ResponseEntity.ok(menuFacade.display(menuId)); + } + + @PutMapping("/{menuId}/hide") + public ResponseEntity hide(@PathVariable final UUID menuId) { + return ResponseEntity.ok(menuService.hide(menuId)); + } + + @GetMapping + public ResponseEntity> findAll() { + return ResponseEntity.ok(menuService.findAll()); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/application/DefaultProfanityChecker.java b/src/main/java/kitchenpos/products/tobe/application/DefaultProfanityChecker.java new file mode 100644 index 000000000..9577fdb3b --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/application/DefaultProfanityChecker.java @@ -0,0 +1,20 @@ +package kitchenpos.products.tobe.application; + +import kitchenpos.products.tobe.domain.ProfanityChecker; +import kitchenpos.products.tobe.infra.PurgomalumClient; +import org.springframework.stereotype.Component; + +@Component +public class DefaultProfanityChecker implements ProfanityChecker { + + private final PurgomalumClient purgomalumClient; + + public DefaultProfanityChecker(PurgomalumClient purgomalumClient) { + this.purgomalumClient = purgomalumClient; + } + + @Override + public boolean containsProfanity(String text) { + return purgomalumClient.containsProfanity(text); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/application/ProductService.java b/src/main/java/kitchenpos/products/tobe/application/ProductService.java index 3181a7f15..4f8468a09 100644 --- a/src/main/java/kitchenpos/products/tobe/application/ProductService.java +++ b/src/main/java/kitchenpos/products/tobe/application/ProductService.java @@ -1,12 +1,9 @@ package kitchenpos.products.tobe.application; -import kitchenpos.products.infra.PurgomalumClient; -import kitchenpos.products.tobe.domain.Product; -import kitchenpos.products.tobe.domain.ProductName; -import kitchenpos.products.tobe.domain.ProductPrice; -import kitchenpos.products.tobe.domain.ProductRepository; +import kitchenpos.products.tobe.domain.*; import kitchenpos.products.tobe.ui.ProductChangePriceRequest; import kitchenpos.products.tobe.ui.ProductCreateRequest; +import kitchenpos.products.tobe.ui.ProductResponse; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,20 +14,20 @@ public class ProductService { private final ProductRepository productRepository; - private final PurgomalumClient purgomalumClient; + private final ProfanityChecker profanityChecker; public ProductService( final ProductRepository productRepository, - final PurgomalumClient purgomalumClient + final ProfanityChecker profanityChecker ) { this.productRepository = productRepository; - this.purgomalumClient = purgomalumClient; + this.profanityChecker = profanityChecker; } @Transactional public Product create(final ProductCreateRequest request) { final var id = UUID.randomUUID(); - final var name = new ProductName(request.name(), purgomalumClient); + final var name = new ProductName(request.name(), profanityChecker); final var price = new ProductPrice(request.price()); return productRepository.save(new Product(id, name, price)); } @@ -46,4 +43,12 @@ public Product changePrice(final UUID productId, final ProductChangePriceRequest public List findAll() { return productRepository.findAll(); } + + @Transactional(readOnly = true) + public List listByIds(final List productIds) { + List products = productRepository.findAllByIdIn(productIds); + return products.stream() + .map(product -> new ProductResponse(product.getId(), product.getPrice().value())) + .toList(); + } } diff --git a/src/main/java/kitchenpos/products/tobe/domain/ProductName.java b/src/main/java/kitchenpos/products/tobe/domain/ProductName.java index c84eed795..f57ec26f2 100644 --- a/src/main/java/kitchenpos/products/tobe/domain/ProductName.java +++ b/src/main/java/kitchenpos/products/tobe/domain/ProductName.java @@ -2,7 +2,6 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import kitchenpos.products.infra.PurgomalumClient; import java.util.Objects; @@ -14,11 +13,11 @@ public class ProductName { protected ProductName() { } - public ProductName(final String name, final PurgomalumClient purgomalumClient) { + public ProductName(final String name, final ProfanityChecker profanityChecker) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("상품명은 필수로 입력해야 합니다."); } - if (purgomalumClient.containsProfanity(name)) { + if (profanityChecker.containsProfanity(name)) { throw new IllegalArgumentException("상품명에 욕설이 포함되어 있습니다."); } this.name = name; diff --git a/src/main/java/kitchenpos/products/tobe/domain/ProfanityChecker.java b/src/main/java/kitchenpos/products/tobe/domain/ProfanityChecker.java new file mode 100644 index 000000000..8c0797d0d --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/domain/ProfanityChecker.java @@ -0,0 +1,5 @@ +package kitchenpos.products.tobe.domain; + +public interface ProfanityChecker { + boolean containsProfanity(String text); +} diff --git a/src/main/java/kitchenpos/products/tobe/infra/DefaultPurgomalumClient.java b/src/main/java/kitchenpos/products/tobe/infra/DefaultPurgomalumClient.java new file mode 100644 index 000000000..c8755ec0b --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/infra/DefaultPurgomalumClient.java @@ -0,0 +1,27 @@ +package kitchenpos.products.tobe.infra; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +@Component("tobeProductContextPurgomalumClient") +public class DefaultPurgomalumClient implements PurgomalumClient { + + private final RestTemplate restTemplate; + + public DefaultPurgomalumClient(final RestTemplateBuilder restTemplateBuilder) { + this.restTemplate = restTemplateBuilder.build(); + } + + @Override + public boolean containsProfanity(final String text) { + final URI url = UriComponentsBuilder.fromUriString("https://www.purgomalum.com/service/containsprofanity") + .queryParam("text", text) + .build() + .toUri(); + return Boolean.parseBoolean(restTemplate.getForObject(url, String.class)); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/infra/PurgomalumClient.java b/src/main/java/kitchenpos/products/tobe/infra/PurgomalumClient.java new file mode 100644 index 000000000..284264d28 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/infra/PurgomalumClient.java @@ -0,0 +1,5 @@ +package kitchenpos.products.tobe.infra; + +public interface PurgomalumClient { + boolean containsProfanity(String text); +} diff --git a/src/main/java/kitchenpos/products/tobe/ui/ProductResponse.java b/src/main/java/kitchenpos/products/tobe/ui/ProductResponse.java new file mode 100644 index 000000000..8f482a73e --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/ui/ProductResponse.java @@ -0,0 +1,7 @@ +package kitchenpos.products.tobe.ui; + +import java.math.BigDecimal; +import java.util.UUID; + +public record ProductResponse(UUID id, BigDecimal price) { +} diff --git a/src/main/java/kitchenpos/products/tobe/ui/ProductRestController.java b/src/main/java/kitchenpos/products/tobe/ui/ProductRestController.java index 0121d62ba..70262777f 100644 --- a/src/main/java/kitchenpos/products/tobe/ui/ProductRestController.java +++ b/src/main/java/kitchenpos/products/tobe/ui/ProductRestController.java @@ -35,4 +35,9 @@ public ResponseEntity changePrice(@PathVariable final UUID productId, @ public ResponseEntity> findAll() { return ResponseEntity.ok(productService.findAll()); } + + @PostMapping("/ids") + public ResponseEntity> listByIds(@RequestBody final List productIds) { + return ResponseEntity.ok(productService.listByIds(productIds)); + } } diff --git a/src/test/java/kitchenpos/products/tobe/application/FakePurgomalumClient.java b/src/test/java/kitchenpos/products/tobe/application/FakePurgomalumClient.java index 73404eef8..1e02591d7 100644 --- a/src/test/java/kitchenpos/products/tobe/application/FakePurgomalumClient.java +++ b/src/test/java/kitchenpos/products/tobe/application/FakePurgomalumClient.java @@ -1,11 +1,11 @@ package kitchenpos.products.tobe.application; -import kitchenpos.products.infra.PurgomalumClient; +import kitchenpos.products.tobe.domain.ProfanityChecker; import java.util.Arrays; import java.util.List; -public class FakePurgomalumClient implements PurgomalumClient { +public class FakePurgomalumClient implements ProfanityChecker { private static final List profanities; static { diff --git a/src/test/java/kitchenpos/products/tobe/application/ProductServiceTest.java b/src/test/java/kitchenpos/products/tobe/application/ProductServiceTest.java index ded5ecd51..5aa041de2 100644 --- a/src/test/java/kitchenpos/products/tobe/application/ProductServiceTest.java +++ b/src/test/java/kitchenpos/products/tobe/application/ProductServiceTest.java @@ -1,10 +1,6 @@ package kitchenpos.products.tobe.application; -import kitchenpos.products.infra.PurgomalumClient; -import kitchenpos.products.tobe.domain.Product; -import kitchenpos.products.tobe.domain.ProductName; -import kitchenpos.products.tobe.domain.ProductPrice; -import kitchenpos.products.tobe.domain.ProductRepository; +import kitchenpos.products.tobe.domain.*; import kitchenpos.products.tobe.ui.ProductChangePriceRequest; import kitchenpos.products.tobe.ui.ProductCreateRequest; import org.jetbrains.annotations.NotNull; @@ -21,21 +17,21 @@ class ProductServiceTest { private ProductRepository productRepository; - private PurgomalumClient purgomalumClient; + private ProfanityChecker profanityChecker; private ProductService productService; @BeforeEach void setUp() { productRepository = new InMemoryProductRepository(); - purgomalumClient = new FakePurgomalumClient(); - productService = new ProductService(productRepository, purgomalumClient); + profanityChecker = new FakePurgomalumClient(); + productService = new ProductService(productRepository, profanityChecker); } @DisplayName("상품을 등록할 수 있다.") @Test void create() { // given - final ProductName expectedName = new ProductName("후라이드", purgomalumClient); + final ProductName expectedName = new ProductName("후라이드", profanityChecker::containsProfanity); final ProductPrice expectedPrice = new ProductPrice(BigDecimal.valueOf(16_000L)); final ProductCreateRequest request = new ProductCreateRequest(expectedName.value(), expectedPrice.value()); @@ -57,7 +53,7 @@ void changePrice() { // given final ProductPrice expectedPrice = new ProductPrice(BigDecimal.valueOf(15_000L)); final ProductChangePriceRequest request = new ProductChangePriceRequest(expectedPrice.value()); - final UUID productId = productRepository.save(createProduct("후라이드", 16_000L, purgomalumClient)).getId(); + final UUID productId = productRepository.save(createProduct("후라이드", 16_000L, profanityChecker)).getId(); // when final Product actual = productService.changePrice(productId, request); @@ -71,8 +67,8 @@ void changePrice() { @Test void findAll() { // given - productRepository.save(createProduct("후라이드", 16_000L, purgomalumClient)); - productRepository.save(createProduct("양념치킨", 16_000L, purgomalumClient)); + productRepository.save(createProduct("후라이드", 16_000L, profanityChecker)); + productRepository.save(createProduct("양념치킨", 16_000L, profanityChecker)); // when final List actual = productService.findAll(); @@ -81,8 +77,8 @@ void findAll() { assertThat(actual).hasSize(2); } - private @NotNull Product createProduct(String name, long price, PurgomalumClient purgomalumClient1) { - final ProductName productName = new ProductName(name, purgomalumClient1); + private @NotNull Product createProduct(String name, long price, ProfanityChecker profanityChecker) { + final ProductName productName = new ProductName(name, profanityChecker); final ProductPrice productPrice = new ProductPrice(BigDecimal.valueOf(price)); return new Product(UUID.randomUUID(), productName, productPrice); }