diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..c201d1457 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,29 @@ +### 기능 요구사항 + +[ ✅ ] 사용자는 자판기가 보유하고 있는 금액을 입력한다. + +[ ✅ ] 자판기가 보유하고 있는 금액으로 동전을 무작위로 생성한다. + +- 투입금액으로 동전을 생성하지 않는다. == Coin클래스로 동전을 생성한다. + +[ ✅ ] 상품명, 가격, 수량을 입력하여 상품을 추가할 수 있다. + +- 상품 가격은 100원부터 시작하며, 10원으로 나누어떨어져야 한다. + +[ ✅ ] 사용자는 금액을 투입할 수 있다. + +[ ✅ ] 사용자가 입력한 투입금액을 기반으로 현재 투입금액을 출력한다. + +[ ✅ ] 사용자는 구매할 상품명을 입력한다. + +[ ✅ ] 잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려준다. + +[ ✅ ] 남은 금액이 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다. + +[ ✅ ] 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다. + +- 반환되지 않은 금액은 자판기에 남는다. + + +## Getter지양 방법 이후 적용하기 +쉽게 말해, getter를 통해 얻은 상태값으로 하려고 했던 '행동'을 그 상태값을 가진 객체가 하도록 '행동'의 주체를 옮기는 것이다. \ No newline at end of file diff --git a/src/main/java/controller/ProductsController.java b/src/main/java/controller/ProductsController.java new file mode 100644 index 000000000..7b2592dd4 --- /dev/null +++ b/src/main/java/controller/ProductsController.java @@ -0,0 +1,74 @@ +package controller; + +import domain.Payment; +import domain.Products; +import dto.ProductNameDto; +import service.ProductsService; +import service.UserPaymentService; +import view.InputView; +import view.OutputView; + +import static util.message.InputMessage.INPUT_PRODUCT_DETAIL; +import static util.message.InputMessage.INPUT_SELECTED_PRODUCT; +import static view.OutputView.printCurrentUserBalance; + +public class ProductsController { + private final ProductsService productsService; + private final UserPaymentService userPaymentService; + private Products products; + + public ProductsController(){ + productsService = new ProductsService(); + userPaymentService = new UserPaymentService(); + } + + public void generateProductInfo(){ + String productInfo = getProductInfo(); + try{ + products = createProducts(productInfo); + } catch (IllegalArgumentException e){ + OutputView.printMessage(e.getMessage()); + generateProductInfo(); + } + } + + private String getProductInfo(){ + return InputView.readConsole(); + } + + private Products createProducts(String productInfo){ + return productsService.createProducts(productInfo); + } + + public void buyProduct(){ + String wantedProduct = getSelectedProduct(); + ProductNameDto productNameDto = createSelectedProduct(wantedProduct); + try{ + productsService.buyProduct(productNameDto); + Payment payment = userPaymentService.getUserPayment(); + OutputView.printCurrentUserBalance(payment.getPayment()); + } catch(IllegalArgumentException e){ + OutputView.printMessage(e.getMessage()); + buyProduct(); + } + } + + private ProductNameDto createSelectedProduct(String wantedProduct){ + return ProductNameDto.create(wantedProduct); + } + + private String getSelectedProduct(){ + OutputView.printMessage(INPUT_SELECTED_PRODUCT.getValue()); + return InputView.readConsole(); + } + + public boolean checkAvailableToPurchase() { + Payment payment = userPaymentService.getUserPayment(); + + boolean isSoldOutOfItemAvailableForBuy = productsService.checkSoldOutOfItemAvailableForBuy(); + boolean isUserBalanceNotEnough = payment.getPayment() < productsService.getMinItemPrice(); + + return !isSoldOutOfItemAvailableForBuy && !isUserBalanceNotEnough; + } +} + diff --git a/src/main/java/controller/UserPaymentController.java b/src/main/java/controller/UserPaymentController.java new file mode 100644 index 000000000..8020ff2df --- /dev/null +++ b/src/main/java/controller/UserPaymentController.java @@ -0,0 +1,39 @@ +package controller; + +import domain.Payment; +import dto.PaymentStatusDto; +import service.UserPaymentService; +import view.InputView; +import view.OutputView; + +import static util.message.InputMessage.INPUT_PAYMENT; + +public class UserPaymentController { + private final UserPaymentService userPaymentService; + + public UserPaymentController(){ + userPaymentService = new UserPaymentService(); + } + + public void generateUserBalance() { + String paymentAmount = getPayment(); + try { + Payment payment = createPayment(paymentAmount); + PaymentStatusDto paymentStatusDto = userPaymentService.createPaymentStatusDto(payment); + OutputView.printPaymentStatus(paymentStatusDto); + } catch (IllegalArgumentException e) { + OutputView.printMessage(e.getMessage()); + generateUserBalance(); + } + } + + private String getPayment(){ + OutputView.printMessage(INPUT_PAYMENT.getValue()); + return InputView.readConsole(); + } + + private Payment createPayment(String paymentAmount){ + return userPaymentService.createPayment(paymentAmount); + } +} + diff --git a/src/main/java/controller/VendingMachineController.java b/src/main/java/controller/VendingMachineController.java new file mode 100644 index 000000000..59e287dfb --- /dev/null +++ b/src/main/java/controller/VendingMachineController.java @@ -0,0 +1,58 @@ +package controller; + +import domain.*; +import dto.CoinsDto; +import dto.VendingMachineStatusDto; +import service.*; +import view.InputView; +import view.OutputView; + +import java.util.List; + +import static util.message.InputMessage.*; + +public class VendingMachineController { + private final PossessionAmountService possesionAmountService; + private final VendingMachineService vendingMachineService; + + public VendingMachineController(){ + possesionAmountService = new PossessionAmountService(); + vendingMachineService = new VendingMachineService(); + } + + public void generateCoins() { + String amount = getPossessionAmount(); + + try { + PossessionAmount possessionAmount = createPossessionAmount(amount); + initCoins(possessionAmount); + } catch (IllegalArgumentException e) { + OutputView.printMessage(e.getMessage()); + generateCoins(); + } + } + + public void initCoins(PossessionAmount possessionAmount){ + vendingMachineService.generateRandomCoins(possessionAmount.getPossessionAmount()); + } + + private String getPossessionAmount(){ + OutputView.printMessage(INPUT_POSSESSION_AMOUNT_MESSAGE.getValue()); + return InputView.readConsole(); + } + + private PossessionAmount createPossessionAmount(String possessionAmount){ + return possesionAmountService.createPossessionAmount(possessionAmount); + } + + public void printChange() { + CoinsDto coinsDto = vendingMachineService.getChange(); + OutputView.printChange(coinsDto); + } + + public void printGeneratedCoins() { + CoinsDto coinsDto = vendingMachineService.getCurrentCoins(); + OutputView.printVendingMachineHoldingCoins(coinsDto); + } + +} diff --git a/src/main/java/domain/Coin.java b/src/main/java/domain/Coin.java new file mode 100644 index 000000000..088cfd33e --- /dev/null +++ b/src/main/java/domain/Coin.java @@ -0,0 +1,57 @@ +package domain; + +import camp.nextstep.edu.missionutils.Randoms; +import util.exception.NoMatchingCoinException; +import util.message.ExceptionMessage; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public enum Coin { + COIN_500(500), + COIN_100(100), + COIN_50(50), + COIN_10(10); + + private final int amount; + + Coin(final int amount) { + this.amount = amount; + } + + public static Coin from(int amount) { + try { + return Arrays.stream(Coin.values()) + .filter(coin -> coin.getAmount() == amount) + .findFirst() + .orElseThrow(() -> new NoMatchingCoinException(ExceptionMessage.NOT_COIN_MESSAGE.getValue())); + } catch (NoMatchingCoinException ex) { + throw new NoMatchingCoinException(String.format(ExceptionMessage.NOT_COIN_MESSAGE.getValue(), amount)); + } + } + + + public static Coin pickRandomWithLimit(int balanceLimit) { + Coin randomCoin = Coin.pickRandom(); + if (randomCoin.getAmount() > balanceLimit) { + return pickRandomWithLimit(balanceLimit); + } + + return randomCoin; + } + + private static Coin pickRandom() { + int pickedAmount = Randoms.pickNumberInList( + Arrays.stream(values()) + .map(Coin::getAmount) + .collect(Collectors.toList()) + ); + + return Coin.from(pickedAmount); + } + + public int getAmount() { + return amount; + } + +} diff --git a/src/main/java/domain/Payment.java b/src/main/java/domain/Payment.java new file mode 100644 index 000000000..323124bae --- /dev/null +++ b/src/main/java/domain/Payment.java @@ -0,0 +1,32 @@ +package domain; + +import domain.wrapper.PaymentAmount; +import domain.wrapper.Price; +import domain.wrapper.VendingMachineAmount; + +public class Payment { + private final PaymentAmount payment; + private Payment(final String payment){ + this.payment = PaymentAmount.create(payment); + } + + private Payment(int payment){ + this.payment = PaymentAmount.create(payment); + } + + public static Payment create(final String payment){ + return new Payment(payment); + } + + public int getPayment(){ + return payment.getPaymentAmount(); + } + + public boolean canBuy(Product product) { + return payment.getPaymentAmount() >= product.getPrice(); + } + + public Payment subtract(int price) { + return new Payment(payment.getPaymentAmount() - price); + } +} diff --git a/src/main/java/domain/PossessionAmount.java b/src/main/java/domain/PossessionAmount.java new file mode 100644 index 000000000..a2302d07e --- /dev/null +++ b/src/main/java/domain/PossessionAmount.java @@ -0,0 +1,19 @@ +package domain; + +import domain.wrapper.VendingMachineAmount; + +public class PossessionAmount { + + private final VendingMachineAmount vendingMachineAmount; + private PossessionAmount(final String possessionAmount){ + this.vendingMachineAmount = VendingMachineAmount.create(possessionAmount); + } + + public static PossessionAmount create(final String possessionAmount){ + return new PossessionAmount(possessionAmount); + } + + public int getPossessionAmount(){ + return vendingMachineAmount.getVendingMachineAmount(); + } +} diff --git a/src/main/java/domain/Product.java b/src/main/java/domain/Product.java new file mode 100644 index 000000000..e926648d8 --- /dev/null +++ b/src/main/java/domain/Product.java @@ -0,0 +1,83 @@ +package domain; + +import domain.wrapper.Name; +import domain.wrapper.Price; +import domain.wrapper.Quantity; + +import java.util.Objects; + +import static domain.constant.ProductConstant.SPLIT_DELIMITER_COMMA; +import static util.message.ExceptionMessage.BLANK_MESSAGE; + +public class Product { + private final Name name; + private final Price price; + private final Quantity quantity; + private static final int SOLD_OUT_QUANTITY = 0; + + private Product(final String productDetail){ + validateBlank(productDetail); + String[] product = splitProduct(productDetail); + this.name = Name.create(product[0]); + this.price = Price.create(product[1]); + this.quantity = Quantity.create(product[2]); + } + + public static Product create(final String productDetail){ + return new Product(productDetail); + } + + private Product(Name name, Price price, Quantity quantity){ + this.name = name; + this.price = price; + this.quantity = quantity; + } + + private String[] splitProduct(final String productDetail){ + return productDetail.split(SPLIT_DELIMITER_COMMA.getValue()); + } + + private void validateBlank(final String productDetail){ + if (productDetail == null || productDetail.trim().isEmpty()) { + throw new IllegalArgumentException(String.format(BLANK_MESSAGE.getValue(), "상품명, 가격, 수량")); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Product product = (Product) o; + return Objects.equals(name, product.name) && + Objects.equals(price, product.price) && + Objects.equals(quantity, product.quantity); + } + + @Override + public int hashCode() { + return Objects.hash(name, price, quantity); + } + + public String getName() { + return name.getName(); + } + + public int getPrice() { + return price.getPrice(); + } + + public int getQuantity() { + return quantity.getQuantity(); + } + + public boolean isSoldOut() { + return quantity.getQuantity() <= SOLD_OUT_QUANTITY; + } + + public Product decreaseQuantity() { + Quantity subtractedQuantity = quantity.subtract(); + return new Product(name, price, subtractedQuantity); + } + +} + diff --git a/src/main/java/domain/Products.java b/src/main/java/domain/Products.java new file mode 100644 index 000000000..7b7f85ebc --- /dev/null +++ b/src/main/java/domain/Products.java @@ -0,0 +1,88 @@ +package domain; + +import util.exception.DuplicateException; + +import java.util.*; +import java.util.stream.Collectors; + +import static domain.constant.ProductsConstant.*; +import static util.message.ExceptionMessage.*; + +public class Products { + private final List products; + + public Products(final String productsInfo){ + validateBlank(productsInfo); + this.products = create(productsInfo); + validateDuplicateProducts(); + validateDuplicateProductName(); + } + + public List getProducts() { + return products; + } + + private void validateBlank(final String productsInfo){ + if (productsInfo == null || productsInfo.trim().isEmpty()) { + throw new IllegalArgumentException(String.format(BLANK_MESSAGE.getValue(), "상품정보")); + } + } + + private List create(final String productsInfo){ + String[] product = splitProducts(productsInfo); + return Arrays.stream(product) + .map(Product::create) + .collect(Collectors.toList()); + } + + private String[] splitProducts(final String productsInfo) { + String[] product = productsInfo.split(SPLIT_DELIMITER_SEMICOLON.getValue()); + for (int i = 0; i < product.length; i++) { + product[i] = product[i].trim().replaceAll("\\[|\\]", BLANK.getValue()); + } + return product; + } + + private void validateDuplicateProducts() { + int uniqueCarCount = new HashSet<>(products).size(); + if (products.size() != uniqueCarCount) { + throw new DuplicateException(String.format(DUPLICATE_MESSAGE.getValue(), "상품정보")); + } + } + + private void validateDuplicateProductName(){ + Set productNames = new HashSet<>(); + for (Product product : products) { + String productName = product.getName().toString(); + if (!productNames.add(productName)) { + throw new DuplicateException(String.format(DUPLICATE_MESSAGE.getValue(), "상품명")); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Products products1 = (Products) o; + return Objects.equals(products, products1.products); + } + + @Override + public int hashCode() { + return Objects.hash(products); + } + + @Override + public String toString(){ + StringBuilder sb = new StringBuilder(); + sb.append("입력한 상품 정보").append('\n'); + for(Product product : products){ + sb.append(product.getName() + " " + product.getPrice() + " " + product.getQuantity()).append('\n'); + } + return sb.toString(); + } + + +} + diff --git a/src/main/java/domain/VendingMachine.java b/src/main/java/domain/VendingMachine.java new file mode 100644 index 000000000..92b80a2e7 --- /dev/null +++ b/src/main/java/domain/VendingMachine.java @@ -0,0 +1,31 @@ +package domain; + +import java.util.HashMap; +import java.util.Map; + +public class VendingMachine { + private final HashMap vendingMachine; + + public VendingMachine(){ + vendingMachine = new HashMap<>(); + init(); + } + + private void init(){ + for (Coin coin : Coin.values()) { + vendingMachine.put(coin, 0); + } + } + + public void addCoins(Coin coin, int count) { + vendingMachine.put(coin, vendingMachine.get(coin) + count); + } + + public int findByCoin(Coin coin) { + return vendingMachine.get(coin); + } + + public Map findAll() { + return vendingMachine; + } +} diff --git a/src/main/java/domain/constant/Constant.java b/src/main/java/domain/constant/Constant.java new file mode 100644 index 000000000..fa3d5ad67 --- /dev/null +++ b/src/main/java/domain/constant/Constant.java @@ -0,0 +1,26 @@ +package domain.constant; + +import util.EnumUtil; + +public enum Constant implements EnumUtil { + COIN_TEN(10), + COIN_HUNDRED(100), + ZERO(0), + ONE_THOUSANE(1000); + + private final int number; + + Constant(final int number){ + this.number = number; + } + + @Override + public String getKey() { + return name(); + } + + @Override + public Integer getValue() { + return number; + } +} diff --git a/src/main/java/domain/constant/ProductConstant.java b/src/main/java/domain/constant/ProductConstant.java new file mode 100644 index 000000000..84d24070c --- /dev/null +++ b/src/main/java/domain/constant/ProductConstant.java @@ -0,0 +1,23 @@ +package domain.constant; + +import util.EnumUtil; + +public enum ProductConstant implements EnumUtil { + SPLIT_DELIMITER_COMMA(","); + + private final String value; + + ProductConstant(final String value) { + this.value = value; + } + + @Override + public String getKey() { + return name(); + } + + @Override + public String getValue() { + return value; + } +} diff --git a/src/main/java/domain/constant/ProductsConstant.java b/src/main/java/domain/constant/ProductsConstant.java new file mode 100644 index 000000000..5ab0d28a2 --- /dev/null +++ b/src/main/java/domain/constant/ProductsConstant.java @@ -0,0 +1,24 @@ +package domain.constant; + +import util.EnumUtil; + +public enum ProductsConstant implements EnumUtil { + SPLIT_DELIMITER_SEMICOLON(";"), + BLANK(""); + + private final String value; + + ProductsConstant(final String value) { + this.value = value; + } + + @Override + public String getKey() { + return name(); + } + + @Override + public String getValue() { + return value; + } +} diff --git a/src/main/java/domain/wrapper/Name.java b/src/main/java/domain/wrapper/Name.java new file mode 100644 index 000000000..5e4ee4923 --- /dev/null +++ b/src/main/java/domain/wrapper/Name.java @@ -0,0 +1,41 @@ +package domain.wrapper; + +import java.util.Objects; + +import static util.message.ExceptionMessage.BLANK_MESSAGE; + +public class Name { + private final String name; + + private Name(final String name){ + validateBlank(name); + this.name = name; + } + + public static Name create(final String name){ + return new Name(name); + } + + private void validateBlank(final String productDetail){ + if (productDetail == null || productDetail.trim().isEmpty()) { + throw new IllegalArgumentException(String.format(BLANK_MESSAGE.getValue(), "상품명")); + } + } + + @Override + public boolean equals(Object diffName) { + if (this == diffName) return true; + if (diffName == null || getClass() != diffName.getClass()) return false; + Name nameInfo = (Name) diffName; + return Objects.equals(name, nameInfo.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/domain/wrapper/PaymentAmount.java b/src/main/java/domain/wrapper/PaymentAmount.java new file mode 100644 index 000000000..b10e37cfa --- /dev/null +++ b/src/main/java/domain/wrapper/PaymentAmount.java @@ -0,0 +1,62 @@ +package domain.wrapper; + +import domain.constant.Constant; + +import static util.message.ExceptionMessage.*; +import static util.message.ExceptionMessage.RANGE_MESSAGE; + +public class PaymentAmount { + private final int paymentAmount; + + private PaymentAmount(final String payment){ + validateNameBlank(payment); + int amount = validateType(payment); + this.paymentAmount = validateDivisibleBy1000(validateRange(amount)); + } + + private PaymentAmount(final int paymentAmount){ + this.paymentAmount = paymentAmount; + } + + public static PaymentAmount create(final String payment){ + return new PaymentAmount(payment); + } + + public static PaymentAmount create(final int paymentAmount){ + return new PaymentAmount(paymentAmount); + } + + public int getPaymentAmount(){ + return paymentAmount; + } + + private void validateNameBlank(final String payment) { + if (payment == null || payment.trim().isEmpty()) { + throw new IllegalArgumentException(String.format(BLANK_MESSAGE.getValue(), "투입금액")); + } + } + + private int validateType(final String amount) { + int count; + try { + count = Integer.parseInt(amount); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format(TYPE_MESSAGE.getValue(), "투입금액")); + } + return count; + } + + private int validateRange(final int amount) { + if (amount <= Constant.ZERO.getValue()) { + throw new IllegalArgumentException(String.format(RANGE_MESSAGE.getValue(), Constant.ZERO.getValue())); + } + return amount; + } + + private int validateDivisibleBy1000(final int amount){ + if(amount % Constant.ONE_THOUSANE.getValue() != Constant.ZERO.getValue()){ + throw new IllegalArgumentException(String.format(UNIT_MESSAGE.getValue(), Constant.ONE_THOUSANE.getValue())); + } + return amount; + } +} diff --git a/src/main/java/domain/wrapper/Price.java b/src/main/java/domain/wrapper/Price.java new file mode 100644 index 000000000..8e2dfc0c7 --- /dev/null +++ b/src/main/java/domain/wrapper/Price.java @@ -0,0 +1,70 @@ +package domain.wrapper; + +import domain.constant.Constant; + +import java.util.Objects; + +import static util.message.ExceptionMessage.*; + +public class Price { + private final int price; + + private Price(final String priceInfo){ + validateBlank(priceInfo); + int amount = validateType(priceInfo); + amount = validateRange(amount); + amount = validateDivisibleBy10(amount); + this.price = amount; + } + + public static Price create(final String priceInfo){ + return new Price(priceInfo); + } + + private void validateBlank(final String productDetail){ + if (productDetail == null || productDetail.trim().isEmpty()) { + throw new IllegalArgumentException(String.format(BLANK_MESSAGE.getValue(), "가격")); + } + } + + private int validateType(final String priceInfo) { + int amount; + try { + amount = Integer.parseInt(priceInfo); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format(TYPE_MESSAGE.getValue(), "가격")); + } + return amount; + } + + private int validateRange(final int amount) { + if (amount < Constant.COIN_HUNDRED.getValue()) { + throw new IllegalArgumentException(String.format(RANGE_MESSAGE.getValue(), Constant.COIN_HUNDRED.getValue())); + } + return amount; + } + + private int validateDivisibleBy10(final int amount){ + if(amount % Constant.COIN_TEN.getValue() != Constant.ZERO.getValue()){ + throw new IllegalArgumentException(String.format(UNIT_MESSAGE.getValue(), Constant.COIN_TEN.getValue())); + } + return amount; + } + + @Override + public boolean equals(Object diffPrice) { + if (this == diffPrice) return true; + if (diffPrice == null || getClass() != diffPrice.getClass()) return false; + Price priceInfo = (Price) diffPrice; + return Objects.equals(price, priceInfo.price); + } + + @Override + public int hashCode() { + return Objects.hash(price); + } + + public int getPrice() { + return price; + } +} diff --git a/src/main/java/domain/wrapper/Quantity.java b/src/main/java/domain/wrapper/Quantity.java new file mode 100644 index 000000000..cd11e6ecb --- /dev/null +++ b/src/main/java/domain/wrapper/Quantity.java @@ -0,0 +1,86 @@ +package domain.wrapper; + +import domain.constant.Constant; +import util.exception.NotEnoughBalanceException; +import util.exception.SoldOutException; +import util.message.ExceptionMessage; + +import java.util.Objects; + +import static util.message.ExceptionMessage.*; + +public class Quantity { + + private static final int SUBTRACT_QUANTITY = 1; + private int quantity; + + private Quantity(final String quantityInfo){ + validateBlank(quantityInfo); + int amount = validateType(quantityInfo); + amount = validateRange(amount); + this.quantity = amount; + } + + private Quantity(int quantity) { + this.quantity = quantity; + } + + public static Quantity create(final String quantityInfo){ + return new Quantity(quantityInfo); + } + + public void add(int amount) { + this.quantity += amount; + } + + private void validateBlank(final String productDetail){ + if (productDetail == null || productDetail.trim().isEmpty()) { + throw new IllegalArgumentException(String.format(BLANK_MESSAGE.getValue(), "수량")); + } + } + + private int validateType(final String priceInfo) { + int amount; + try { + amount = Integer.parseInt(priceInfo); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format(TYPE_MESSAGE.getValue(), "수량")); + } + return amount; + } + + private int validateRange(final int amount) { + if (amount <= Constant.ZERO.getValue()) { + throw new IllegalArgumentException(String.format(RANGE_MESSAGE.getValue(), Constant.ZERO.getValue())); + } + return amount; + } + + @Override + public boolean equals(Object diffQuantity) { + if (this == diffQuantity) return true; + if (diffQuantity == null || getClass() != diffQuantity.getClass()) return false; + Quantity quantityInfo = (Quantity) diffQuantity; + return Objects.equals(quantity, quantityInfo.quantity); + } + + @Override + public int hashCode() { + return Objects.hash(quantity); + } + + public int getQuantity() { + return quantity; + } + + public Quantity subtract() { + validateAbleToSubtractItemQuantity(quantity); + return new Quantity(quantity - SUBTRACT_QUANTITY); + } + + private void validateAbleToSubtractItemQuantity(int quantity){ + if (quantity <= 0) { + throw new SoldOutException(); + } + } +} diff --git a/src/main/java/domain/wrapper/VendingMachineAmount.java b/src/main/java/domain/wrapper/VendingMachineAmount.java new file mode 100644 index 000000000..ebc4b454a --- /dev/null +++ b/src/main/java/domain/wrapper/VendingMachineAmount.java @@ -0,0 +1,53 @@ +package domain.wrapper; + +import domain.constant.Constant; + +import static util.message.ExceptionMessage.*; + +public class VendingMachineAmount { + private final int vendingMachineAmount; + + private VendingMachineAmount(final String possesionAmount){ + validateNameBlank(possesionAmount); + int amount = validateType(possesionAmount); + validateRange(amount); + validateDivisibleBy10(amount); + this.vendingMachineAmount = amount; + } + + public static VendingMachineAmount create(final String possesionAmount){ + return new VendingMachineAmount(possesionAmount); + } + + public int getVendingMachineAmount(){ + return vendingMachineAmount; + } + + private void validateNameBlank(final String possesionAmount) { + if (possesionAmount == null || possesionAmount.trim().isEmpty()) { + throw new IllegalArgumentException(String.format(BLANK_MESSAGE.getValue(), "보유금액")); + } + } + + private int validateType(final String amount) { + int count; + try { + count = Integer.parseInt(amount); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format(TYPE_MESSAGE.getValue(), "보유금액")); + } + return count; + } + + private void validateDivisibleBy10(final int amount){ + if(amount % Constant.COIN_TEN.getValue() != Constant.ZERO.getValue()){ + throw new IllegalArgumentException(String.format(UNIT_MESSAGE.getValue(), Constant.COIN_TEN.getValue())); + } + } + + private void validateRange(final int amount) { + if (amount < Constant.ZERO.getValue()) { + throw new IllegalArgumentException(String.format(RANGE_MESSAGE.getValue(), Constant.ZERO.getValue())); + } + } +} diff --git a/src/main/java/dto/CoinsDto.java b/src/main/java/dto/CoinsDto.java new file mode 100644 index 000000000..1eb615aa9 --- /dev/null +++ b/src/main/java/dto/CoinsDto.java @@ -0,0 +1,54 @@ +package dto; + +import domain.Coin; + +import java.util.Map; + +public class CoinsDto { + int coin500Quantity; + int coin100Quantity; + int coin50Quantity; + int coin10Quantity; + + private CoinsDto(int coin500Quantity, int coin100Quantity, int coin50Quantity, int coin10Quantity) { + this.coin500Quantity = coin500Quantity; + this.coin100Quantity = coin100Quantity; + this.coin50Quantity = coin50Quantity; + this.coin10Quantity = coin10Quantity; + } + + public static CoinsDto from(Map coins) { + return new CoinsDto( + coins.get(Coin.COIN_500), + coins.get(Coin.COIN_100), + coins.get(Coin.COIN_50), + coins.get(Coin.COIN_10) + ); + } + + public int getCoin500Quantity() { + return coin500Quantity; + } + + public int getCoin100Quantity() { + return coin100Quantity; + } + + public int getCoin50Quantity() { + return coin50Quantity; + } + + public int getCoin10Quantity() { + return coin10Quantity; + } + + public int getAllCoinQuantity() { + return coin500Quantity + coin100Quantity + coin50Quantity + coin10Quantity; + } + + public String toString(){ + StringBuilder sb = new StringBuilder(); + sb.append(coin500Quantity + "," + coin100Quantity + "," + coin50Quantity + "," + coin10Quantity); + return sb.toString(); + } +} diff --git a/src/main/java/dto/PaymentStatusDto.java b/src/main/java/dto/PaymentStatusDto.java new file mode 100644 index 000000000..ca8dab1ae --- /dev/null +++ b/src/main/java/dto/PaymentStatusDto.java @@ -0,0 +1,17 @@ +package dto; + +public class PaymentStatusDto { + private final int payment; + + public PaymentStatusDto(final int payment){ + this.payment = payment; + } + + public static PaymentStatusDto create(final int payment) { + return new PaymentStatusDto(payment); + } + + public int getPayment(){ + return payment; + } +} diff --git a/src/main/java/dto/ProductNameDto.java b/src/main/java/dto/ProductNameDto.java new file mode 100644 index 000000000..8b2bc9589 --- /dev/null +++ b/src/main/java/dto/ProductNameDto.java @@ -0,0 +1,19 @@ +package dto; + +import domain.wrapper.Name; + +public class ProductNameDto { + private final String productName; + + private ProductNameDto(String productName){ + this.productName = productName; + } + + public static ProductNameDto create(String productName){ + return new ProductNameDto(productName); + } + + public Name toEntity(){ + return Name.create(productName); + } +} diff --git a/src/main/java/dto/VendingMachineStatusDto.java b/src/main/java/dto/VendingMachineStatusDto.java new file mode 100644 index 000000000..0605b9606 --- /dev/null +++ b/src/main/java/dto/VendingMachineStatusDto.java @@ -0,0 +1,15 @@ +package dto; + +public class VendingMachineStatusDto { + private final int coin; + private final int count; + + private VendingMachineStatusDto(final int coin, final int count){ + this.coin = coin; + this.count = count; + } + + public static VendingMachineStatusDto create(final int coin, final int count) { + return new VendingMachineStatusDto(coin, count); + } +} diff --git a/src/main/java/repository/ProductsRepository.java b/src/main/java/repository/ProductsRepository.java new file mode 100644 index 000000000..e481c7127 --- /dev/null +++ b/src/main/java/repository/ProductsRepository.java @@ -0,0 +1,48 @@ +package repository; + +import domain.Product; +import domain.Products; +import domain.wrapper.Name; +import util.exception.DuplicateException; +import util.exception.NoResourceException; + +import static util.message.ExceptionMessage.DUPLICATE_MESSAGE; +import static util.message.ExceptionMessage.NO_RESOURCE_MESSAGE; + +public class ProductsRepository { + + private static final ProductsRepository instance = new ProductsRepository(); + private Products products; + + private ProductsRepository(){ + + } + + //다음 거에 적용할 부분! + public static ProductsRepository getInstance(){ + return instance; + } + + public Products findAll() { + return products; + } + + public Products createProducts(final String productsInfo){ + this.products = new Products(productsInfo); + return products; + } + public void updateByItemName(Name name, Product product) { + Product originalItem = findByProductName(name); + int itemIndex = products.getProducts().indexOf(originalItem); + products.getProducts().set(itemIndex, product); + } + + public Product findByProductName(Name name) { + return products.getProducts().stream() + .filter(product -> product.getName().equals(name.getName())) + .findFirst() + .orElseThrow(() -> + new NoResourceException(String.format(NO_RESOURCE_MESSAGE.getValue(), "해당 상품"))); + } + +} diff --git a/src/main/java/repository/UserPaymentRepository.java b/src/main/java/repository/UserPaymentRepository.java new file mode 100644 index 000000000..8cf775900 --- /dev/null +++ b/src/main/java/repository/UserPaymentRepository.java @@ -0,0 +1,34 @@ +package repository; + +import domain.Payment; +import dto.PaymentStatusDto; + +public class UserPaymentRepository { + private static final UserPaymentRepository userPaymentRepository = new UserPaymentRepository(); + private Payment userPayment; + + private UserPaymentRepository(){ + + } + + public static UserPaymentRepository getInstance() { + return userPaymentRepository; + } + + public Payment createPayment(final String payment){ + this.userPayment = Payment.create(payment); + return userPayment; + } + + public PaymentStatusDto createPaymentStatusDto(final Payment payment) { + return PaymentStatusDto.create(payment.getPayment()); + } + + public Payment get() { + return userPayment; + } + + public void update(Payment payment) { + this.userPayment = payment; + } +} diff --git a/src/main/java/repository/VendingMachineRepository.java b/src/main/java/repository/VendingMachineRepository.java new file mode 100644 index 000000000..b4fed5e2c --- /dev/null +++ b/src/main/java/repository/VendingMachineRepository.java @@ -0,0 +1,32 @@ +package repository; + +import domain.Coin; +import domain.VendingMachine; + +import java.util.HashMap; +import java.util.Map; + +public class VendingMachineRepository { + private static final VendingMachineRepository vendingMachineRepository = new VendingMachineRepository(); + private VendingMachine vendingMachine; + + private VendingMachineRepository() { + vendingMachine = new VendingMachine(); + } + + public static VendingMachineRepository getInstance() { + return vendingMachineRepository; + } + + public void addCoins(Coin coin, int coinQuantity) { + vendingMachine.addCoins(coin, coinQuantity); + } + + public int findByCoin(Coin coin) { + return vendingMachine.findByCoin(coin); + } + + public Map findAll() { + return vendingMachine.findAll(); + } +} diff --git a/src/main/java/service/PossessionAmountService.java b/src/main/java/service/PossessionAmountService.java new file mode 100644 index 000000000..83fa25155 --- /dev/null +++ b/src/main/java/service/PossessionAmountService.java @@ -0,0 +1,9 @@ +package service; + +import domain.PossessionAmount; + +public class PossessionAmountService { + public PossessionAmount createPossessionAmount(final String possessionAmount){ + return PossessionAmount.create(possessionAmount); + } +} diff --git a/src/main/java/service/ProductsService.java b/src/main/java/service/ProductsService.java new file mode 100644 index 000000000..2badeba25 --- /dev/null +++ b/src/main/java/service/ProductsService.java @@ -0,0 +1,59 @@ +package service; + +import domain.Payment; +import domain.Product; +import domain.Products; +import domain.wrapper.Name; +import dto.ProductNameDto; +import repository.ProductsRepository; +import repository.UserPaymentRepository; +import util.exception.NoMatchingCoinException; +import util.exception.NotEnoughBalanceException; +import util.message.ExceptionMessage; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class ProductsService { + + private final UserPaymentRepository userPaymentRepository = UserPaymentRepository.getInstance(); + private final ProductsRepository productsRepository = ProductsRepository.getInstance(); + + public Products createProducts(final String productsInfo){ + return productsRepository.createProducts(productsInfo); + } + + public boolean checkSoldOutOfItemAvailableForBuy() { + Payment payment = userPaymentRepository.get(); + Products products = productsRepository.findAll(); + return products.getProducts().stream() + .filter(product -> Integer.valueOf(product.getPrice()) <= payment.getPayment()) + .allMatch(Product::isSoldOut); + } + + public Integer getMinItemPrice() { + Products products = productsRepository.findAll(); + List productPrices = products.getProducts().stream() + .map(Product::getPrice) + .collect(Collectors.toList()); + + return Collections.min(productPrices); + } + + public void buyProduct(ProductNameDto productNameDto) { + Name productName = productNameDto.toEntity(); + Product product = productsRepository.findByProductName(productName); + + Payment userPayment = userPaymentRepository.get(); + if (!userPayment.canBuy(product)) { + throw new NotEnoughBalanceException(); + } + + Product purchasedProduct = product.decreaseQuantity(); + + userPaymentRepository.update(userPayment.subtract(product.getPrice())); + productsRepository.updateByItemName(productName, purchasedProduct); + } + +} diff --git a/src/main/java/service/UserPaymentService.java b/src/main/java/service/UserPaymentService.java new file mode 100644 index 000000000..74c7c179b --- /dev/null +++ b/src/main/java/service/UserPaymentService.java @@ -0,0 +1,22 @@ +package service; + +import domain.Payment; +import dto.PaymentStatusDto; +import repository.UserPaymentRepository; + +public class UserPaymentService { + + private final UserPaymentRepository userPaymentRepository = UserPaymentRepository.getInstance(); + + public Payment createPayment(final String payment){ + return userPaymentRepository.createPayment(payment); + } + + public PaymentStatusDto createPaymentStatusDto(final Payment payment) { + return userPaymentRepository.createPaymentStatusDto(payment); + } + + public Payment getUserPayment(){ + return userPaymentRepository.get(); + } +} diff --git a/src/main/java/service/VendingMachineService.java b/src/main/java/service/VendingMachineService.java new file mode 100644 index 000000000..de1d0b694 --- /dev/null +++ b/src/main/java/service/VendingMachineService.java @@ -0,0 +1,65 @@ +package service; + +import domain.*; +import dto.CoinsDto; +import dto.VendingMachineStatusDto; +import repository.UserPaymentRepository; +import repository.VendingMachineRepository; +import view.OutputView; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class VendingMachineService { + + private static final int REMAINING_BALANCE_WHEN_ALL_COINS_GENERATED = 0; + + private final VendingMachineRepository vendingMachineRepository = VendingMachineRepository.getInstance(); + + private final UserPaymentRepository userPaymentRepository = UserPaymentRepository.getInstance(); + + public CoinsDto getCurrentCoins() { + Map coins = vendingMachineRepository.findAll(); + return CoinsDto.from(coins); + } + + public void generateRandomCoins(int possessionAmount) { + + while (possessionAmount > REMAINING_BALANCE_WHEN_ALL_COINS_GENERATED) { + Coin randomCoin = Coin.pickRandomWithLimit(possessionAmount); + + vendingMachineRepository.addCoins(randomCoin, 1); + + possessionAmount = possessionAmount - randomCoin.getAmount(); + } + } + + public CoinsDto getChange() { + Map changeCoins = new HashMap<>(); + int remainingBalance = userPaymentRepository.get().getPayment(); + + for (Coin coin : Coin.values()) { + int quantity = getCoinQuantityForChange(coin, remainingBalance); + changeCoins.put(coin, quantity); + + remainingBalance = remainingBalance - (coin.getAmount() * quantity); + } + + return CoinsDto.from(changeCoins); + } + + private int getCoinQuantityForChange(Coin coin, int balance) { + int maxCoinQuantityForChange = getMaxCoinQuantityForChange(coin, balance); + int holdingQuantity = vendingMachineRepository.findByCoin(coin); + + return Math.min(maxCoinQuantityForChange, holdingQuantity); + } + + private int getMaxCoinQuantityForChange(Coin coin, int balance) { + return balance / coin.getAmount(); + } + +} + diff --git a/src/main/java/util/EnumUtil.java b/src/main/java/util/EnumUtil.java new file mode 100644 index 000000000..e0c0bcb60 --- /dev/null +++ b/src/main/java/util/EnumUtil.java @@ -0,0 +1,6 @@ +package util; + +public interface EnumUtil { + T1 getKey(); + T2 getValue(); +} diff --git a/src/main/java/util/exception/ConsoleException.java b/src/main/java/util/exception/ConsoleException.java new file mode 100644 index 000000000..2c43267b1 --- /dev/null +++ b/src/main/java/util/exception/ConsoleException.java @@ -0,0 +1,7 @@ +package util.exception; + +public class ConsoleException extends GlobalException{ + public ConsoleException(final String message) { + super(message); + } +} diff --git a/src/main/java/util/exception/DuplicateException.java b/src/main/java/util/exception/DuplicateException.java new file mode 100644 index 000000000..d5929500f --- /dev/null +++ b/src/main/java/util/exception/DuplicateException.java @@ -0,0 +1,7 @@ +package util.exception; + +public class DuplicateException extends GlobalException{ + public DuplicateException(final String message) { + super(message); + } +} diff --git a/src/main/java/util/exception/GlobalException.java b/src/main/java/util/exception/GlobalException.java new file mode 100644 index 000000000..12ab0f809 --- /dev/null +++ b/src/main/java/util/exception/GlobalException.java @@ -0,0 +1,7 @@ +package util.exception; + +public class GlobalException extends RuntimeException{ + public GlobalException(final String message) { + super(message); + } +} diff --git a/src/main/java/util/exception/NoMatchingCoinException.java b/src/main/java/util/exception/NoMatchingCoinException.java new file mode 100644 index 000000000..4a50214fc --- /dev/null +++ b/src/main/java/util/exception/NoMatchingCoinException.java @@ -0,0 +1,7 @@ +package util.exception; + +public class NoMatchingCoinException extends GlobalException{ + public NoMatchingCoinException(final String message) { + super(message); + } +} diff --git a/src/main/java/util/exception/NoResourceException.java b/src/main/java/util/exception/NoResourceException.java new file mode 100644 index 000000000..2f9ec9df2 --- /dev/null +++ b/src/main/java/util/exception/NoResourceException.java @@ -0,0 +1,7 @@ +package util.exception; + +public class NoResourceException extends GlobalException{ + public NoResourceException(String message){ + super(message); + } +} diff --git a/src/main/java/util/exception/NotEnoughBalanceException.java b/src/main/java/util/exception/NotEnoughBalanceException.java new file mode 100644 index 000000000..7ffe3db4f --- /dev/null +++ b/src/main/java/util/exception/NotEnoughBalanceException.java @@ -0,0 +1,8 @@ +package util.exception; + +public class NotEnoughBalanceException extends IllegalArgumentException{ + public static final String ERROR_MESSAGE = "해당 상품을 구매하기에 잔여 투입 금액이 부족합니다."; + public NotEnoughBalanceException() { + super(ERROR_MESSAGE); + } +} diff --git a/src/main/java/util/exception/SoldOutException.java b/src/main/java/util/exception/SoldOutException.java new file mode 100644 index 000000000..a0894ceae --- /dev/null +++ b/src/main/java/util/exception/SoldOutException.java @@ -0,0 +1,9 @@ +package util.exception; + +public class SoldOutException extends IllegalArgumentException{ + + public static final String ERROR_MESSAGE = "품절된 상품은 구매할 수 없습니다."; + public SoldOutException() { + super(ERROR_MESSAGE); + } +} diff --git a/src/main/java/util/message/ExceptionMessage.java b/src/main/java/util/message/ExceptionMessage.java new file mode 100644 index 000000000..289fb691a --- /dev/null +++ b/src/main/java/util/message/ExceptionMessage.java @@ -0,0 +1,33 @@ +package util.message; + +import util.EnumUtil; + +public enum ExceptionMessage implements EnumUtil { + BLANK_MESSAGE("%s은(는) 빈 값이 들어올 수 없습니다.") + , TYPE_MESSAGE("%s은(는) 숫자만 입력할 수 있습니다.") + , RANGE_MESSAGE("%d 보다 큰 값을 입력해 주세요.") + , UNIT_MESSAGE("%d원 단위로 입력해 주세요.") + , DUPLICATE_MESSAGE("%s을(를) 중복으로 입력할 수 없습니다.") + , NO_RESOURCE_MESSAGE("%s(이)가 존재하지 않습니다.") + , NOT_COIN_MESSAGE("해당하는 %d원 동전이 존재하지 않습니다."); + private static final String ERROR_TAG = "[ERROR] "; + private final String message; + + public String getMessage(){ + return message; + } + + ExceptionMessage(final String message) { + this.message = ERROR_TAG + message; + } + + @Override + public String getKey() { + return name(); + } + + @Override + public String getValue() { + return message; + } +} diff --git a/src/main/java/util/message/InputMessage.java b/src/main/java/util/message/InputMessage.java new file mode 100644 index 000000000..7cc6a79e8 --- /dev/null +++ b/src/main/java/util/message/InputMessage.java @@ -0,0 +1,27 @@ +package util.message; + +import util.EnumUtil; + +public enum InputMessage implements EnumUtil { + + INPUT_POSSESSION_AMOUNT_MESSAGE("자판기가 보유하고 있는 금액을 입력해 주세요."), + INPUT_PRODUCT_DETAIL("\n상품명과 가격, 수량을 입력해 주세요."), + INPUT_PAYMENT("\n투입 금액을 입력해 주세요."), + INPUT_SELECTED_PRODUCT("구매할 상품명을 입력해 주세요."); + + private final String message; + + InputMessage(final String message) { + this.message = message; + } + + @Override + public String getKey() { + return name(); + } + + @Override + public String getValue() { + return message; + } +} diff --git a/src/main/java/util/message/OutputMessage.java b/src/main/java/util/message/OutputMessage.java new file mode 100644 index 000000000..03d657efb --- /dev/null +++ b/src/main/java/util/message/OutputMessage.java @@ -0,0 +1,32 @@ +package util.message; + +import util.EnumUtil; + +public enum OutputMessage implements EnumUtil { + VENDING_MACHINE_STATUS("\n자판기가 보유한 동전"), + PAYMENT_AMOUNT("\n투입 금액: %d원"), + COIN_OUTPUT_FORMAT("%s - %s개"), + CHANGE_MESSAGE("잔돈"), + NO_CHANGE_MESSAGE("반환할 잔돈이 없습니다."), + COIN_500("500원"), + COIN_100("100원"), + COIN_50("50원"), + COIN_10("10원"); + + + private final String message; + + OutputMessage(final String message) { + this.message = message; + } + + @Override + public String getKey() { + return name(); + } + + @Override + public String getValue() { + return message; + } +} diff --git a/src/main/java/vendingmachine/Application.java b/src/main/java/vendingmachine/Application.java index 9d3be447b..5ba022820 100644 --- a/src/main/java/vendingmachine/Application.java +++ b/src/main/java/vendingmachine/Application.java @@ -1,7 +1,34 @@ package vendingmachine; +import controller.ProductsController; +import controller.UserPaymentController; +import controller.VendingMachineController; + public class Application { + private static final VendingMachineController vendingMachineController = new VendingMachineController(); + private static final ProductsController productsController = new ProductsController(); + private static final UserPaymentController userPaymentController = new UserPaymentController(); + public static void main(String[] args) { - // TODO: 프로그램 구현 + init(); + purchaseProduct(); + printChange(); + } + + private static void init(){ + vendingMachineController.generateCoins(); + vendingMachineController.printGeneratedCoins(); + productsController.generateProductInfo(); + userPaymentController.generateUserBalance(); + } + + private static void purchaseProduct(){ + while (productsController.checkAvailableToPurchase()) { + productsController.buyProduct(); + } + } + + private static void printChange() { + vendingMachineController.printChange(); } } diff --git a/src/main/java/vendingmachine/Coin.java b/src/main/java/vendingmachine/Coin.java deleted file mode 100644 index c76293fbc..000000000 --- a/src/main/java/vendingmachine/Coin.java +++ /dev/null @@ -1,16 +0,0 @@ -package vendingmachine; - -public enum Coin { - COIN_500(500), - COIN_100(100), - COIN_50(50), - COIN_10(10); - - private final int amount; - - Coin(final int amount) { - this.amount = amount; - } - - // 추가 기능 구현 -} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 000000000..b38330908 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,10 @@ +package view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + + public static String readConsole(){ + return Console.readLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 000000000..7e1fbafc8 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,59 @@ +package view; + +import domain.constant.Constant; +import dto.CoinsDto; +import dto.PaymentStatusDto; +import util.message.OutputMessage; + +import static com.sun.javafx.font.FontResource.ZERO; +import static domain.Coin.*; +import static util.message.OutputMessage.*; +import static util.message.OutputMessage.COIN_10; +import static util.message.OutputMessage.COIN_100; +import static util.message.OutputMessage.COIN_50; +import static util.message.OutputMessage.COIN_500; + +public class OutputView { + public static void printMessage(String message) { + System.out.println(message); + } + + private static void printCoin(String type, int coinQuantity) { + System.out.println(String.format(COIN_OUTPUT_FORMAT.getValue(), type, coinQuantity)); + } + + public static void printPaymentStatus(PaymentStatusDto paymentStatusDto){ + System.out.println(String.format(PAYMENT_AMOUNT.getValue(), paymentStatusDto.getPayment())); + } + + public static void printCurrentUserBalance(int userBalance) { + System.out.println(String.format(PAYMENT_AMOUNT.getValue(), userBalance)); + } + + public static void printChange(CoinsDto coinsDto) { + System.out.println(CHANGE_MESSAGE.getValue()); + + printCoinIgnoringZero(COIN_500.getValue(), coinsDto.getCoin500Quantity()); + printCoinIgnoringZero(COIN_100.getValue(), coinsDto.getCoin100Quantity()); + printCoinIgnoringZero(COIN_50.getValue(), coinsDto.getCoin50Quantity()); + printCoinIgnoringZero(COIN_10.getValue(), coinsDto.getCoin10Quantity()); + + if (coinsDto.getAllCoinQuantity() == ZERO) { + System.out.println(NO_CHANGE_MESSAGE.getValue()); + } + } + + private static void printCoinIgnoringZero(String type, int coinQuantity) { + if (coinQuantity > Constant.ZERO.getValue()) { + System.out.println(String.format(COIN_OUTPUT_FORMAT.getValue(), type, coinQuantity)); + } + } + + public static void printVendingMachineHoldingCoins(CoinsDto coinsDto) { + System.out.println(VENDING_MACHINE_STATUS.getValue()); + printCoin(COIN_500.getValue(), coinsDto.getCoin500Quantity()); + printCoin(COIN_100.getValue(), coinsDto.getCoin100Quantity()); + printCoin(COIN_50.getValue(), coinsDto.getCoin50Quantity()); + printCoin(COIN_10.getValue(), coinsDto.getCoin10Quantity()); + } +} diff --git a/src/test/java/domain/ProductsTest.java b/src/test/java/domain/ProductsTest.java new file mode 100644 index 000000000..945a4d9d3 --- /dev/null +++ b/src/test/java/domain/ProductsTest.java @@ -0,0 +1,70 @@ +package domain; + +import domain.Products; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import provider.TestProvider; +import util.exception.DuplicateException; +import util.exception.GlobalException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static util.message.ExceptionMessage.BLANK_MESSAGE; +import static util.message.ExceptionMessage.DUPLICATE_MESSAGE; + +public class ProductsTest { + private Products testProducts; + + @BeforeEach + void init() { + String testProductInfos = "[콜라,1500,20];[사이다,1000,10]"; + testProducts = TestProvider.createTestProducts(testProductInfos); + } + + @ParameterizedTest + @ValueSource(strings = {"[콜라,1500,20];[사이다,1000,10]"}) + @DisplayName("정상적인 상품정보를 입력하면, 예외가 발생하지 않는다.") + void givenNormalProductInfos_thenSuccess(final String testProductInfos) { + // when & then + assertThatCode(() -> new Products(testProductInfos)) + .doesNotThrowAnyException(); + + assertThat(new Products(testProductInfos)) + .isEqualTo(testProducts); + } + + @ParameterizedTest + @NullSource + @DisplayName("상품정보에 null 값이 들어오면 split 시 예외가 발생한다.") + void givenNullProductInfos_thenFail(final String testProductInfos) { + assertThatThrownBy(() -> new Products(testProductInfos)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(BLANK_MESSAGE.getValue(), "상품정보")); + } + + @ParameterizedTest + @ValueSource(strings = {"[콜라,1500,20];[콜라,1500,20]"}) + @DisplayName("상품정보에 중복값이 들어오면 예외가 발생한다.") + void givenDuplicateProducts_thenFail(final String testProductInfos) { + assertThatThrownBy(() -> new Products(testProductInfos)) + .isInstanceOf(GlobalException.class) + .isExactlyInstanceOf(DuplicateException.class) + .hasMessageContaining(String.format(DUPLICATE_MESSAGE.getValue(), "상품정보")); + } + + @ParameterizedTest + @ValueSource(strings = {"[콜라,1500,20];[콜라,1500,10]"}) + @DisplayName("상품명에 중복값이 들어오면 예외가 발생한다.") + void givenDuplicateProductName_thenFail(final String testProductInfos) { + assertThatThrownBy(() -> new Products(testProductInfos)) + .isInstanceOf(GlobalException.class) + .isExactlyInstanceOf(DuplicateException.class) + .hasMessageContaining(String.format(DUPLICATE_MESSAGE.getValue(), "상품명")); + } +} diff --git a/src/test/java/domain/wrapper/PaymentAmountTest.java b/src/test/java/domain/wrapper/PaymentAmountTest.java new file mode 100644 index 000000000..57dba8354 --- /dev/null +++ b/src/test/java/domain/wrapper/PaymentAmountTest.java @@ -0,0 +1,62 @@ +package domain.wrapper; + +import domain.constant.Constant; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static util.message.ExceptionMessage.*; +import static util.message.ExceptionMessage.RANGE_MESSAGE; + +public class PaymentAmountTest { + @ParameterizedTest + @DisplayName("투입금액을 올바르게 입력한 경우 예외가 발생하지 않는다.") + @CsvSource("4000") + void givenNormalPaymentAmount_thenSuccess(final String paymentAmount) { + assertThat(PaymentAmount.create(paymentAmount)) + .isInstanceOf(PaymentAmount.class); + + assertThatCode(() -> PaymentAmount.create(paymentAmount)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @DisplayName("투입금액을 빈값으로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"", " ", " ", " ", " ", "\n", "\t", "\r"}) + void givenBlankPaymentAmount_thenFail(final String paymentAmount) { + assertThatThrownBy(() -> PaymentAmount.create(paymentAmount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(BLANK_MESSAGE.getValue(), "투입금액")); + } + + @ParameterizedTest + @DisplayName("투입금액을 숫자가 아닌 형태로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"abc", "12bd"}) + void givenNonNumeric_thenFail(final String paymentAmount) { + assertThatThrownBy(() -> PaymentAmount.create(paymentAmount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(TYPE_MESSAGE.getValue(), "투입금액")); + } + + @ParameterizedTest + @DisplayName("투입금액이 0이하인경우 예외가 발생한다.") + @ValueSource(strings = {"-1", "0"}) + void givenLessZero_thenFail(final String paymentAmount) { + assertThatThrownBy(() -> PaymentAmount.create(paymentAmount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(RANGE_MESSAGE.getValue(), Constant.ZERO.getValue())); + } + + @ParameterizedTest + @DisplayName("투입금액이 1000으로 나누어 떨어지지 않는 경우 예외가 발생한다.") + @ValueSource(strings = {"4565", "1223"}) + void givenNonDivisibleBy1000_thenFail(final String amount) { + assertThatThrownBy(() -> PaymentAmount.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(UNIT_MESSAGE.getValue(), Constant.ONE_THOUSANE.getValue())); + } +} diff --git a/src/test/java/domain/wrapper/ProductNameTest.java b/src/test/java/domain/wrapper/ProductNameTest.java new file mode 100644 index 000000000..270b20e6c --- /dev/null +++ b/src/test/java/domain/wrapper/ProductNameTest.java @@ -0,0 +1,33 @@ +package domain.wrapper; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static util.message.ExceptionMessage.BLANK_MESSAGE; + +public class ProductNameTest { + @ParameterizedTest + @DisplayName("정상적으로 입력했을 경우 예외를 처리하지 않는다.") + @ValueSource(strings = {"콜라", "사이다", "피자", "치킨"}) + void givenNormalName_thenSuccess(final String carName) { + assertThat(Name.create(carName)) + .isInstanceOf(Name.class); + + assertThatCode(() -> Name.create(carName)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @DisplayName("이름이 공백일 경우 예외를 처리한다.") + @EmptySource + void givenBlankName_thenFail(final String carName) { + assertThatThrownBy(() -> Name.create(carName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(BLANK_MESSAGE.getValue(), "상품명")); + } +} diff --git a/src/test/java/domain/wrapper/ProductPriceTest.java b/src/test/java/domain/wrapper/ProductPriceTest.java new file mode 100644 index 000000000..449bb49bc --- /dev/null +++ b/src/test/java/domain/wrapper/ProductPriceTest.java @@ -0,0 +1,62 @@ +package domain.wrapper; + +import domain.constant.Constant; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static util.message.ExceptionMessage.*; +import static util.message.ExceptionMessage.RANGE_MESSAGE; + +public class ProductPriceTest { + @ParameterizedTest + @DisplayName("상품가격을 올바르게 입력한 경우 예외가 발생하지 않는다.") + @CsvSource("450") + void givenNormalPrice_thenSuccess(final String amount) { + assertThat(Price.create(amount)) + .isInstanceOf(Price.class); + + assertThatCode(() -> Price.create(amount)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @DisplayName("상품가격을 빈값으로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"", " ", " ", " ", " ", "\n", "\t", "\r"}) + void givenBlankPrice_thenFail(final String amount) { + assertThatThrownBy(() -> Price.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(BLANK_MESSAGE.getValue(), "가격")); + } + + @ParameterizedTest + @DisplayName("상품가격을 숫자가 아닌 형태로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"abc", "12bd"}) + void givenNonNumeric_thenFail(final String amount) { + assertThatThrownBy(() -> Price.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(TYPE_MESSAGE.getValue(), "가격")); + } + + @ParameterizedTest + @DisplayName("가격이 100원보다 낮은경우 예외가 발생한다.") + @ValueSource(strings = {"-1", "80"}) + void givenLessZero_thenFail(final String amount) { + assertThatThrownBy(() -> Price.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(RANGE_MESSAGE.getValue(), Constant.COIN_HUNDRED.getValue())); + } + + @ParameterizedTest + @DisplayName("가격이 10으로 나누어 떨어지지 않는 경우 예외가 발생한다.") + @ValueSource(strings = {"456", "123"}) + void givenNonDivisibleBy10_thenFail(final String amount) { + assertThatThrownBy(() -> Price.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(UNIT_MESSAGE.getValue(), Constant.COIN_TEN.getValue())); + } +} diff --git a/src/test/java/domain/wrapper/ProductQuantityTest.java b/src/test/java/domain/wrapper/ProductQuantityTest.java new file mode 100644 index 000000000..68cc72366 --- /dev/null +++ b/src/test/java/domain/wrapper/ProductQuantityTest.java @@ -0,0 +1,52 @@ +package domain.wrapper; + +import domain.constant.Constant; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static util.message.ExceptionMessage.*; + +public class ProductQuantityTest { + @ParameterizedTest + @DisplayName("상품수량을 올바르게 입력한 경우 예외가 발생하지 않는다.") + @CsvSource("450") + void givenNormalQuantity_thenSuccess(final String amount) { + assertThat(Quantity.create(amount)) + .isInstanceOf(Quantity.class); + + assertThatCode(() -> Quantity.create(amount)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @DisplayName("상품수량을 빈값으로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"", " ", " ", " ", " ", "\n", "\t", "\r"}) + void givenBlankQuantity_thenFail(final String amount) { + assertThatThrownBy(() -> Quantity.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(BLANK_MESSAGE.getValue(), "수량")); + } + + @ParameterizedTest + @DisplayName("상품수량을 숫자가 아닌 형태로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"abc", "12bd"}) + void givenNonNumeric_thenFail(final String amount) { + assertThatThrownBy(() -> Quantity.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(TYPE_MESSAGE.getValue(), "수량")); + } + + @ParameterizedTest + @DisplayName("수량이 0이하인경우 예외가 발생한다.") + @ValueSource(strings = {"-1", "0"}) + void givenLessZero_thenFail(final String amount) { + assertThatThrownBy(() -> Quantity.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(RANGE_MESSAGE.getValue(), Constant.ZERO.getValue())); + } +} diff --git a/src/test/java/domain/wrapper/VendingMachineAmountTest.java b/src/test/java/domain/wrapper/VendingMachineAmountTest.java new file mode 100644 index 000000000..48e41a560 --- /dev/null +++ b/src/test/java/domain/wrapper/VendingMachineAmountTest.java @@ -0,0 +1,61 @@ +package domain.wrapper; + +import domain.constant.Constant; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static util.message.ExceptionMessage.*; + +public class VendingMachineAmountTest { + @ParameterizedTest + @DisplayName("보유금액을 올바르게 입력한 경우 예외가 발생하지 않는다.") + @CsvSource("450") + void givenNormalAmount_thenSuccess(final String amount) { + assertThat(VendingMachineAmount.create(amount)) + .isInstanceOf(VendingMachineAmount.class); + + assertThatCode(() -> VendingMachineAmount.create(amount)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @DisplayName("보유금액을 빈값으로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"", " ", " ", " ", " ", "\n", "\t", "\r"}) + void givenBlankAmount_thenFail(final String amount) { + assertThatThrownBy(() -> VendingMachineAmount.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(BLANK_MESSAGE.getValue(), "보유금액")); + } + + @ParameterizedTest + @DisplayName("보유금액을 숫자가 아닌 형태로 입력한 경우 예외가 발생한다.") + @ValueSource(strings = {"abc", "12bd"}) + void givenNonNumeric_thenFail(final String amount) { + assertThatThrownBy(() -> VendingMachineAmount.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(TYPE_MESSAGE.getValue(), "보유금액")); + } + + @ParameterizedTest + @DisplayName("보유금액이 10으로 나누어 떨어지지 않는 경우 예외가 발생한다.") + @ValueSource(strings = {"456", "123"}) + void givenNonDivisibleBy10_thenFail(final String amount) { + assertThatThrownBy(() -> VendingMachineAmount.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(UNIT_MESSAGE.getValue(), Constant.COIN_TEN.getValue())); + } + + @ParameterizedTest + @DisplayName("보유금액이 0이하인경우 예외가 발생한다.") + @ValueSource(strings = {"-1", "-3"}) + void givenLessZero_thenFail(final String amount) { + assertThatThrownBy(() -> VendingMachineAmount.create(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format(RANGE_MESSAGE.getValue(), Constant.ZERO.getValue())); + } +} diff --git a/src/test/java/provider/TestProvider.java b/src/test/java/provider/TestProvider.java new file mode 100644 index 000000000..516294dcd --- /dev/null +++ b/src/test/java/provider/TestProvider.java @@ -0,0 +1,12 @@ +package provider; + +import domain.Product; +import domain.Products; + +import java.util.List; + +public class TestProvider { + public static Products createTestProducts(final String productsInfo) { + return new Products(productsInfo); + } +}