diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..75c82b58d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,107 @@ +## 기능 목록 +- [x] 자판기 보유 금액 생성 + - [x] 자판기 보유 금액 입력 요청 메시지 출력 + - [x] 자판기 보유 금액 입력 + - [x] 숫자인지 검증 + - [x] 10원으로 나누어 떨어지는지 검증 +- [x] 자판기 보유 동전 생성 + - [x] 자판기 보유 동전 랜덤 생성 + - [x] 자판기 보유 동전 저장 + - [x] 자판기 보유 동전 출력 +- [x] 상품 가격 및 수량 저장 + - [x] 상품명, 가격, 수량 입력 요청 메시지 출력 + - [x] 상품명, 가격, 수량 입력 + - [x] 대괄호로 묶어 세미콜론으로 구분했는지 검증 + - [x] 상품 저장 +- [x] 투입 금액 설정 + - [x] 투입 금액 입력 요청 메시지 출력 + - [x] 투입 금액 입력 + - [x] 숫자인지 검증 + - [x] 투입 금액 저장 +- [x] 상품 구매 + - [x] 구매할 상품명 입력 요청 메시지 출력 + - [x] 구매할 상품명 입력 + - [x] 상품 구매 + - [x] 금액 차감 + - [x] 차감된 투입 금액 출력 +- [x] 남은 금액이 최저 가격보다 적거나, 모든 상품 소진시 잔돈 반환 + - [x] 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환 (잔액 부족시 or 나눠 떨어지지 않을시) + - [x] 잔돈 동전별로 출력 + +## 구현 클래스 목록 + +- VendingMachineController + - start() + +- OutputView + - printVendingMachineMoney() + - printCoins() + - printProductRequest() + - printUserMoneyRequest() + - printBuyProductRequest() + - printRemainingMoney() + +- InputView + - readVendingMachineMoney() + - readProducts() + - readUserMoney() + - readBuyProduct() + +- InputManager + - readVendingMachineMoney() + - readProducts() + - readUserMoney() + +- InputValidator + - validateNumeric() + - validateProducts() + +- VendingMachineService + - makeCoins + - saveProducts() + - saveUserMoney() + - findRemainingUserMoney() + - purchaseProduct() + +- VendingMachineRepository + - saveCoins() + - findCoins() + - saveProducts() + - findProducts() + - saveUserMoney() + - findUserMoney() + +- RandomCoinGenerator + - generate() + +- VendingMachineMoney + - getValue() + - hasMoney() + - minusValue() + +- Coins + - addCoin() + +- Product + - getName() + - getPrice() + - hasSameName() + - purchase() + +- Products + - hasLowerPrice() + - getProduct() + +- UserMoney + - getAmount() + - hasRemainingMoney() + - decrease() + +- BuyProduct + - getName() + +## 열거형 목록 +- Coin +- VendingMachineMessage +- ErrorMessage +- BuyStatus diff --git a/src/main/java/vendingmachine/Application.java b/src/main/java/vendingmachine/Application.java index 9d3be447b..698c7bf65 100644 --- a/src/main/java/vendingmachine/Application.java +++ b/src/main/java/vendingmachine/Application.java @@ -1,7 +1,9 @@ package vendingmachine; +import vendingmachine.controller.VendingMachineController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + new VendingMachineController().start(); } } 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/vendingmachine/constant/BuyStatus.java b/src/main/java/vendingmachine/constant/BuyStatus.java new file mode 100644 index 000000000..6fd6e7883 --- /dev/null +++ b/src/main/java/vendingmachine/constant/BuyStatus.java @@ -0,0 +1,9 @@ +package vendingmachine.constant; + +public enum BuyStatus { + CONTINUE, FINISHED; + + public boolean isContinue() { + return this == CONTINUE; + } +} diff --git a/src/main/java/vendingmachine/constant/Coin.java b/src/main/java/vendingmachine/constant/Coin.java new file mode 100644 index 000000000..18b5fc54f --- /dev/null +++ b/src/main/java/vendingmachine/constant/Coin.java @@ -0,0 +1,38 @@ +package vendingmachine.constant; + +import vendingmachine.domain.VendingMachineMoney; + +import java.util.Arrays; +import java.util.List; +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 List getCoinByVendingMachineMoney(final VendingMachineMoney vendingMachineMoney) { + return Arrays.stream(values()) + .filter(value -> value.amount <= vendingMachineMoney.getValue()) + .map(coin -> coin.amount) + .collect(Collectors.toList()); + } + + public static Coin valueOfAmount(final int amount) { + return Arrays.stream(values()) + .filter(value -> value.amount == amount) + .findFirst() + .orElseThrow(() -> new IllegalStateException(ErrorMessage.INVALID_AMOUNT.getMessage())); + } + + public int getAmount() { + return this.amount; + } +} diff --git a/src/main/java/vendingmachine/constant/ErrorMessage.java b/src/main/java/vendingmachine/constant/ErrorMessage.java new file mode 100644 index 000000000..821312532 --- /dev/null +++ b/src/main/java/vendingmachine/constant/ErrorMessage.java @@ -0,0 +1,22 @@ +package vendingmachine.constant; + +public enum ErrorMessage { + NOT_NUMERIC("입력값은 숫자만 가능합니다."), + INVALID_MONEY("숫자가 단위로 떨어지지 않습니다."), + INVALID_AMOUNT("잘못된 Amount 입니다."), + INVALID_PRODUCTS_INPUT("잘못된 Product 입력입니다."), + NEGATIVE_NUMBER("음수는 받을 수 없습니다."), + PRODUCT_NOT_EXISTS("존재하지 않는 상품입니다."), + NOT_ENOUGH_MONEY("잔액이 충분하지 않습니다."); + + private static final String ERROR_PREFIX = "[ERROR] "; + private final String message; + + ErrorMessage(final String message) { + this.message = message; + } + + public String getMessage() { + return ERROR_PREFIX + message; + } +} diff --git a/src/main/java/vendingmachine/constant/VendingMachineMessage.java b/src/main/java/vendingmachine/constant/VendingMachineMessage.java new file mode 100644 index 000000000..5b9e1860b --- /dev/null +++ b/src/main/java/vendingmachine/constant/VendingMachineMessage.java @@ -0,0 +1,22 @@ +package vendingmachine.constant; + +public enum VendingMachineMessage { + + VENDING_MACHINE_MONEY_REQUEST("자판기가 보유하고 있는 금액을 입력해 주세요."), + VENDING_MACHINE_COINS("자판기가 보유한 동전"), + PRODUCT_REQUEST("상품명과 가격, 수량을 입력해 주세요."), + USER_MONEY_REQUEST("투입 금액을 입력해 주세요."), + REMAINING_MONEY("투입 금액: %d원"), + BUY_PRODUCT_REQUEST("구매할 상품명을 입력해 주세요."), + CHANGE_MONEY("잔돈"); + + private final String message; + + VendingMachineMessage(final String message) { + this.message = message; + } + + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/vendingmachine/controller/VendingMachineController.java b/src/main/java/vendingmachine/controller/VendingMachineController.java new file mode 100644 index 000000000..3fc44cbbd --- /dev/null +++ b/src/main/java/vendingmachine/controller/VendingMachineController.java @@ -0,0 +1,66 @@ +package vendingmachine.controller; + +import vendingmachine.constant.BuyStatus; +import vendingmachine.domain.*; +import vendingmachine.io.InputManager; +import vendingmachine.io.OutputView; +import vendingmachine.service.VendingMachineService; + +public class VendingMachineController { + + private final OutputView outputView; + private final InputManager inputManager; + private final VendingMachineService vendingMachineService; + + public VendingMachineController() { + this.outputView = new OutputView(); + this.inputManager = new InputManager(); + this.vendingMachineService = new VendingMachineService(); + } + + + public void start() { + makeVendingMachineMoney(); + makeProduct(); + makeUserMoney(); + buyProducts(); + changeCoins(); + } + + private void changeCoins() { + final UserMoney userMoney = vendingMachineService.findRemainingUserMoney(); + outputView.printRemainingMoney(userMoney); + final Coins coins = vendingMachineService.changeMoney(userMoney); + outputView.printChangeMoney(coins); + } + + private void buyProducts() { + BuyStatus buyStatus = BuyStatus.CONTINUE; + + while (buyStatus.isContinue()) { + final UserMoney userMoney = vendingMachineService.findRemainingUserMoney(); + outputView.printBuyProductRequest(userMoney); + final BuyProduct buyProduct = inputManager.readBuyProduct(); + buyStatus = vendingMachineService.purchaseProduct(buyProduct, userMoney); + } + } + + private void makeUserMoney() { + outputView.printUserMoneyRequest(); + final UserMoney userMoney = inputManager.readUserMoney(); + vendingMachineService.saveUserMoney(userMoney); + } + + private void makeProduct() { + outputView.printProductRequest(); + final Products products = inputManager.readProducts(); + vendingMachineService.saveProducts(products); + } + + private void makeVendingMachineMoney() { + outputView.printVendingMachineMoneyRequest(); + final VendingMachineMoney vendingMachineMoney = inputManager.readVendingMachineMoney(); + final Coins coins = vendingMachineService.makeCoins(vendingMachineMoney); + outputView.printCoins(coins); + } +} diff --git a/src/main/java/vendingmachine/domain/BuyProduct.java b/src/main/java/vendingmachine/domain/BuyProduct.java new file mode 100644 index 000000000..791cc5b5f --- /dev/null +++ b/src/main/java/vendingmachine/domain/BuyProduct.java @@ -0,0 +1,14 @@ +package vendingmachine.domain; + +public class BuyProduct { + + private final String name; + + public BuyProduct(final String name) { + this.name = name; + } + + public String getName() { + return this.name; + } +} diff --git a/src/main/java/vendingmachine/domain/Coins.java b/src/main/java/vendingmachine/domain/Coins.java new file mode 100644 index 000000000..b5558f2da --- /dev/null +++ b/src/main/java/vendingmachine/domain/Coins.java @@ -0,0 +1,50 @@ +package vendingmachine.domain; + +import vendingmachine.constant.Coin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Coins { + + private static final String COIN_FORMAT = "%d원 - %d개\n"; + private final List coins; + + + public Coins() { + this.coins = new ArrayList<>(); + } + + public void addCoin(final Coin coin) { + this.coins.add(coin); + } + + public String getCoinMessage() { + return Arrays.stream(Coin.values()) + .map(c -> { + final long count = this.coins.stream().filter(coin -> c == coin).count(); + return String.format(COIN_FORMAT, c.getAmount(), count); + }).collect(Collectors.joining()); + } + + public Coins calculateCoin(final UserMoney userMoney) { + int userMoneyAmount = userMoney.getAmount(); + return getCalculateResult(userMoneyAmount); + } + + private Coins getCalculateResult(int userMoneyAmount) { + final Coins result = new Coins(); + + for (final Coin coin : this.coins) { + final int coinAmount = coin.getAmount(); + if (coinAmount > userMoneyAmount) { + continue; + } + userMoneyAmount -= coinAmount; + result.addCoin(coin); + } + return result; + } +} diff --git a/src/main/java/vendingmachine/domain/Product.java b/src/main/java/vendingmachine/domain/Product.java new file mode 100644 index 000000000..36dee3415 --- /dev/null +++ b/src/main/java/vendingmachine/domain/Product.java @@ -0,0 +1,52 @@ +package vendingmachine.domain; + +import vendingmachine.constant.ErrorMessage; + +public class Product { + + private static final int NO_QUANTITY = 0; + private static final int ZERO_PRICE = 0; + private final String name; + private final Integer price; + private Integer quantity; + + public Product(final String name, final Integer price, final Integer quantity) { + validateNumbers(price, quantity); + this.name = name; + this.price = price; + this.quantity = quantity; + } + + private void validateNumbers(final Integer price, final Integer quantity) { + if (price <= ZERO_PRICE || quantity <= NO_QUANTITY) { + throw new IllegalArgumentException(ErrorMessage.NEGATIVE_NUMBER.getMessage()); + } + } + + public String getName() { + return this.name; + } + + public Integer getPrice() { + return this.price; + } + + public boolean hasSameName(final BuyProduct buyProduct) { + return this.name.equals(buyProduct.getName()); + } + + public void purchase() { + validateQuantity(); + this.quantity--; + } + + private void validateQuantity() { + if (this.quantity <= NO_QUANTITY) { + throw new IllegalStateException(ErrorMessage.PRODUCT_NOT_EXISTS.getMessage()); + } + } + + public boolean hasQuantity() { + return this.quantity > NO_QUANTITY; + } +} diff --git a/src/main/java/vendingmachine/domain/Products.java b/src/main/java/vendingmachine/domain/Products.java new file mode 100644 index 000000000..ca91a540d --- /dev/null +++ b/src/main/java/vendingmachine/domain/Products.java @@ -0,0 +1,65 @@ +package vendingmachine.domain; + +import vendingmachine.constant.BuyStatus; +import vendingmachine.constant.ErrorMessage; + +import java.util.Collections; +import java.util.List; + +public class Products { + + private final List products; + + public Products(final List products) { + validateUniqueName(products); + this.products = Collections.unmodifiableList(products); + } + + private void validateUniqueName(final List products) { + if (isUnique(products)) { + throw new IllegalArgumentException(ErrorMessage.INVALID_PRODUCTS_INPUT.getMessage()); + } + } + + private boolean isUnique(final List products) { + return products.stream().map(Product::getName).distinct().count() != products.size(); + } + + public Product getProduct(final BuyProduct buyProduct) { + validateProduct(buyProduct); + return products.stream() + .filter(product -> product.hasSameName(buyProduct)) + .findFirst() + .orElseThrow(() -> new IllegalStateException(ErrorMessage.PRODUCT_NOT_EXISTS.getMessage())); + } + + private void validateProduct(final BuyProduct buyProduct) { + if (hasNoMatchingProduct(buyProduct)) { + throw new IllegalArgumentException(ErrorMessage.PRODUCT_NOT_EXISTS.getMessage()); + } + } + + private boolean hasNoMatchingProduct(final BuyProduct buyProduct) { + return this.products.stream().map(Product::getName).noneMatch(name -> name.equals(buyProduct.getName())); + } + + public boolean isPurchasable(final UserMoney userMoney) { + return this.products.stream().anyMatch(product -> product.hasQuantity() && product.getPrice() <= userMoney.getAmount()); + } + + public BuyStatus purchaseProduct(final UserMoney userMoney, final Product product) { + if (isPurchasable(userMoney)) { + return purchase(userMoney, product); + } + return BuyStatus.FINISHED; + } + + private BuyStatus purchase(final UserMoney userMoney, final Product product) { + userMoney.decrease(product); + product.purchase(); + if (isPurchasable(userMoney)) { + return BuyStatus.CONTINUE; + } + return BuyStatus.FINISHED; + } +} diff --git a/src/main/java/vendingmachine/domain/UserMoney.java b/src/main/java/vendingmachine/domain/UserMoney.java new file mode 100644 index 000000000..e54a29aa6 --- /dev/null +++ b/src/main/java/vendingmachine/domain/UserMoney.java @@ -0,0 +1,35 @@ +package vendingmachine.domain; + +import vendingmachine.constant.ErrorMessage; + +public class UserMoney { + + private static final int NO_MONEY = 0; + private Integer amount; + + public UserMoney(final int amount) { + validatePositive(amount); + this.amount = amount; + } + + private void validatePositive(final int amount) { + if (amount <= NO_MONEY) { + throw new IllegalArgumentException(ErrorMessage.INVALID_AMOUNT.getMessage()); + } + } + + public int getAmount() { + return this.amount; + } + + public void decrease(final Product product) { + validateMoney(product.getPrice()); + this.amount -= product.getPrice(); + } + + private void validateMoney(final Integer amount) { + if (this.amount < amount) { + throw new IllegalStateException(ErrorMessage.NOT_ENOUGH_MONEY.getMessage()); + } + } +} diff --git a/src/main/java/vendingmachine/domain/VendingMachineMoney.java b/src/main/java/vendingmachine/domain/VendingMachineMoney.java new file mode 100644 index 000000000..8e8ea0bdf --- /dev/null +++ b/src/main/java/vendingmachine/domain/VendingMachineMoney.java @@ -0,0 +1,44 @@ +package vendingmachine.domain; + +import vendingmachine.constant.ErrorMessage; + +public class VendingMachineMoney { + + private static final int MONEY_UNIT = 10; + private static final int NO_REMAINING_MONEY = 0; + private Integer value; + + public VendingMachineMoney(final int value) { + validateMoneyUnit(value); + this.value = value; + } + + private void validateMoneyUnit(final int value) { + if (value % MONEY_UNIT != 0) { + throw new IllegalArgumentException(ErrorMessage.INVALID_MONEY.getMessage()); + } + } + + public int getValue() { + return this.value; + } + + public boolean hasMoney() { + return this.value != NO_REMAINING_MONEY; + } + + public void minusValue(final int coin) { + validateCoin(coin); + this.value -= coin; + } + + private void validateCoin(final int coin) { + if (!isAvailableCoin(coin)) { + throw new IllegalStateException(ErrorMessage.INVALID_AMOUNT.getMessage()); + } + } + + public boolean isAvailableCoin(final int coin) { + return this.value >= coin; + } +} diff --git a/src/main/java/vendingmachine/io/InputManager.java b/src/main/java/vendingmachine/io/InputManager.java new file mode 100644 index 000000000..72c0d9798 --- /dev/null +++ b/src/main/java/vendingmachine/io/InputManager.java @@ -0,0 +1,61 @@ +package vendingmachine.io; + +import vendingmachine.domain.*; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class InputManager { + + private static final String PRODUCT_DELIMITER = ";"; + private static final String FIELD_DELIMITER = ","; + private static final int DELIMITER_INDEX = 1; + private static final int NAME_INDEX = 0; + private static final int PRICE_INDEX = 1; + private static final int QUANTITY_INDEX = 2; + private final InputView inputView; + + public InputManager() { + this.inputView = new InputView(); + } + + public VendingMachineMoney readVendingMachineMoney() { + return read(() -> new VendingMachineMoney(Integer.parseInt(inputView.readVendingMachineMoney()))); + } + + public Products readProducts() { + return read(() -> { + final String input = inputView.readProducts(); + final List products = Arrays.stream(input.split(PRODUCT_DELIMITER)) + .map(i -> i.substring(DELIMITER_INDEX, i.length() - DELIMITER_INDEX)) + .map(s -> { + final String[] strings = s.split(FIELD_DELIMITER); + return new Product( + strings[NAME_INDEX], + Integer.parseInt(strings[PRICE_INDEX]), + Integer.parseInt(strings[QUANTITY_INDEX])); + }).collect(Collectors.toList()); + return new Products(products); + }); + } + + public UserMoney readUserMoney() { + return read(() -> new UserMoney(Integer.parseInt(inputView.readUserMoney()))); + } + + public BuyProduct readBuyProduct() { + return read(() -> new BuyProduct(inputView.readBuyProduct())); + } + + private T read(final Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + } +} diff --git a/src/main/java/vendingmachine/io/InputValidator.java b/src/main/java/vendingmachine/io/InputValidator.java new file mode 100644 index 000000000..d961daf2f --- /dev/null +++ b/src/main/java/vendingmachine/io/InputValidator.java @@ -0,0 +1,32 @@ +package vendingmachine.io; + +import vendingmachine.constant.ErrorMessage; + +public class InputValidator { + + private static final Character MIN_NUMBER_STANDARD = '0'; + private static final Character MAX_NUMBER_STANDARD = '9'; + private static final String START_UNIT = "["; + private static final String DELIMITER = ","; + private static final String END_UNIT = "]"; + + public void validateNumeric(final String input) { + if (isNotNumeric(input)) { + throw new IllegalArgumentException(ErrorMessage.NOT_NUMERIC.getMessage()); + } + } + + private boolean isNotNumeric(final String input) { + return input.chars().anyMatch(c -> c < MIN_NUMBER_STANDARD || c > MAX_NUMBER_STANDARD); + } + + public void validateProducts(final String input) { + if (isInvalidProductsInput(input)) { + throw new IllegalArgumentException(ErrorMessage.INVALID_PRODUCTS_INPUT.getMessage()); + } + } + + private boolean isInvalidProductsInput(final String input) { + return !input.startsWith(START_UNIT) || !input.endsWith(END_UNIT) || !input.contains(DELIMITER); + } +} diff --git a/src/main/java/vendingmachine/io/InputView.java b/src/main/java/vendingmachine/io/InputView.java new file mode 100644 index 000000000..36de3cd80 --- /dev/null +++ b/src/main/java/vendingmachine/io/InputView.java @@ -0,0 +1,34 @@ +package vendingmachine.io; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + + private final InputValidator inputValidator; + + public InputView() { + this.inputValidator = new InputValidator(); + } + + public String readVendingMachineMoney() { + final String input = Console.readLine(); + inputValidator.validateNumeric(input); + return input; + } + + public String readProducts() { + final String input = Console.readLine(); + inputValidator.validateProducts(input); + return input; + } + + public String readUserMoney() { + final String input = Console.readLine(); + inputValidator.validateNumeric(input); + return input; + } + + public String readBuyProduct() { + return Console.readLine(); + } +} diff --git a/src/main/java/vendingmachine/io/OutputView.java b/src/main/java/vendingmachine/io/OutputView.java new file mode 100644 index 000000000..fb21a38dc --- /dev/null +++ b/src/main/java/vendingmachine/io/OutputView.java @@ -0,0 +1,39 @@ +package vendingmachine.io; + +import vendingmachine.constant.VendingMachineMessage; +import vendingmachine.domain.Coins; +import vendingmachine.domain.UserMoney; + +public class OutputView { + + public void printVendingMachineMoneyRequest() { + System.out.println(VendingMachineMessage.VENDING_MACHINE_MONEY_REQUEST.getMessage()); + } + + public void printCoins(final Coins coins) { + System.out.println(VendingMachineMessage.VENDING_MACHINE_COINS.getMessage()); + System.out.println(coins.getCoinMessage()); + } + + public void printProductRequest() { + System.out.println(VendingMachineMessage.PRODUCT_REQUEST.getMessage()); + } + + public void printUserMoneyRequest() { + System.out.println(VendingMachineMessage.USER_MONEY_REQUEST.getMessage()); + } + + public void printBuyProductRequest(final UserMoney userMoney) { + printRemainingMoney(userMoney); + System.out.println(VendingMachineMessage.BUY_PRODUCT_REQUEST.getMessage()); + } + + public void printRemainingMoney(final UserMoney userMoney) { + System.out.println(String.format(VendingMachineMessage.REMAINING_MONEY.getMessage(), userMoney.getAmount())); + } + + public void printChangeMoney(final Coins coins) { + System.out.println(VendingMachineMessage.CHANGE_MONEY.getMessage()); + System.out.println(coins.getCoinMessage()); + } +} diff --git a/src/main/java/vendingmachine/repository/VendingMachineRepository.java b/src/main/java/vendingmachine/repository/VendingMachineRepository.java new file mode 100644 index 000000000..4463e77be --- /dev/null +++ b/src/main/java/vendingmachine/repository/VendingMachineRepository.java @@ -0,0 +1,37 @@ +package vendingmachine.repository; + +import vendingmachine.domain.Coins; +import vendingmachine.domain.Products; +import vendingmachine.domain.UserMoney; + +public class VendingMachineRepository { + + private Coins coins; + private Products products; + private UserMoney userMoney; + + public Coins saveCoins(final Coins coins) { + this.coins = coins; + return this.coins; + } + + public Coins findCoins() { + return this.coins; + } + + public void saveProducts(final Products products) { + this.products = products; + } + + public Products findProducts() { + return this.products; + } + + public void saveUserMoney(final UserMoney userMoney) { + this.userMoney = userMoney; + } + + public UserMoney findUserMoney() { + return this.userMoney; + } +} diff --git a/src/main/java/vendingmachine/service/VendingMachineService.java b/src/main/java/vendingmachine/service/VendingMachineService.java new file mode 100644 index 000000000..49c7a44e8 --- /dev/null +++ b/src/main/java/vendingmachine/service/VendingMachineService.java @@ -0,0 +1,46 @@ +package vendingmachine.service; + +import vendingmachine.constant.BuyStatus; +import vendingmachine.domain.*; +import vendingmachine.repository.VendingMachineRepository; +import vendingmachine.utils.RandomCoinGenerator; + +public class VendingMachineService { + + private final VendingMachineRepository vendingMachineRepository; + private final RandomCoinGenerator randomCoinGenerator; + + public VendingMachineService() { + this.vendingMachineRepository = new VendingMachineRepository(); + this.randomCoinGenerator = new RandomCoinGenerator(); + } + + public Coins makeCoins(final VendingMachineMoney vendingMachineMoney) { + final Coins coins = randomCoinGenerator.generate(vendingMachineMoney); + return vendingMachineRepository.saveCoins(coins); + } + + public void saveProducts(final Products products) { + vendingMachineRepository.saveProducts(products); + } + + public void saveUserMoney(final UserMoney userMoney) { + vendingMachineRepository.saveUserMoney(userMoney); + } + + public UserMoney findRemainingUserMoney() { + return vendingMachineRepository.findUserMoney(); + } + + public BuyStatus purchaseProduct(final BuyProduct buyProduct, final UserMoney userMoney) { + final Products products = vendingMachineRepository.findProducts(); + final Product product = products.getProduct(buyProduct); + + return products.purchaseProduct(userMoney, product); + } + + public Coins changeMoney(final UserMoney userMoney) { + final Coins coins = vendingMachineRepository.findCoins(); + return coins.calculateCoin(userMoney); + } +} diff --git a/src/main/java/vendingmachine/utils/RandomCoinGenerator.java b/src/main/java/vendingmachine/utils/RandomCoinGenerator.java new file mode 100644 index 000000000..1db9e2c52 --- /dev/null +++ b/src/main/java/vendingmachine/utils/RandomCoinGenerator.java @@ -0,0 +1,28 @@ +package vendingmachine.utils; + +import camp.nextstep.edu.missionutils.Randoms; +import vendingmachine.constant.Coin; +import vendingmachine.domain.Coins; +import vendingmachine.domain.VendingMachineMoney; + +import java.util.List; + +public class RandomCoinGenerator { + + public Coins generate(final VendingMachineMoney vendingMachineMoney) { + final List availableCoins = Coin.getCoinByVendingMachineMoney(vendingMachineMoney); + return addCoins(vendingMachineMoney, availableCoins); + } + + private Coins addCoins(final VendingMachineMoney vendingMachineMoney, final List availableCoins) { + final Coins coins = new Coins(); + while (vendingMachineMoney.hasMoney()) { + final int coin = Randoms.pickNumberInList(availableCoins); + if (vendingMachineMoney.isAvailableCoin(coin)) { + vendingMachineMoney.minusValue(coin); + coins.addCoin(Coin.valueOfAmount(coin)); + } + } + return coins; + } +} diff --git a/src/test/java/vendingmachine/domain/CoinsTest.java b/src/test/java/vendingmachine/domain/CoinsTest.java new file mode 100644 index 000000000..b3629ef74 --- /dev/null +++ b/src/test/java/vendingmachine/domain/CoinsTest.java @@ -0,0 +1,30 @@ +package vendingmachine.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vendingmachine.constant.Coin; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("코인 일급컬렉션에서") +class CoinsTest { + + @Test + @DisplayName("남은 코인 계산 과정에서 가능한 코인이 없으면 모두 0개로 표시한다") + void calculateCoin() { + //given + Coins coins = new Coins(); + coins.addCoin(Coin.COIN_100); + + //when + final Coins result = coins.calculateCoin(new UserMoney(50)); + + //then + System.out.println(result.getCoinMessage()); + assertThat(result.getCoinMessage()).contains( + "500원 - 0개\n" + + "100원 - 0개\n" + + "50원 - 0개\n" + + "10원 - 0개"); + } +} diff --git a/src/test/java/vendingmachine/domain/ProductTest.java b/src/test/java/vendingmachine/domain/ProductTest.java new file mode 100644 index 000000000..64350f241 --- /dev/null +++ b/src/test/java/vendingmachine/domain/ProductTest.java @@ -0,0 +1,33 @@ +package vendingmachine.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("제품 도메인에서") +class ProductTest { + + @Test + @DisplayName("가격이 음수일 경우 생성시 예외를 던진다") + void constructorWithMinusPrice() { + assertThatThrownBy(() -> new Product("abc", -1, 10)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("가격이 음수일 경우 생성시 예외를 던진다") + void constructorWithMinusQuantity() { + assertThatThrownBy(() -> new Product("abc", 100, -1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void purchase() { + assertThatThrownBy(() -> { + final Product product = new Product("abc", 100, 1); + product.purchase(); + product.purchase(); + }).isInstanceOf(IllegalStateException.class); + } +} diff --git a/src/test/java/vendingmachine/domain/ProductsTest.java b/src/test/java/vendingmachine/domain/ProductsTest.java new file mode 100644 index 000000000..42c50f31b --- /dev/null +++ b/src/test/java/vendingmachine/domain/ProductsTest.java @@ -0,0 +1,104 @@ +package vendingmachine.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import vendingmachine.constant.BuyStatus; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("제품 일급 컬렉션에서") +class ProductsTest { + + @Test + @DisplayName("같은 이름의 제품 존재시 예외를 던진다") + void constructor() { + assertThatThrownBy(() -> new Products(Arrays.asList(new Product("abc", 20, 20), new Product("abc", 10, 10)))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("원하는 이름의 제품이 존재하지 않으면 예외를 던진다") + void getProduct() { + assertThatThrownBy(() -> new Products(Arrays.asList(new Product("abc", 20, 20), new Product("def", 10, 10))) + .getProduct(new BuyProduct("ghi"))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("가격에 비해 UserMoney의 Amount가 낮은 경우 False를 반환한다") + void isPurchasableWithInvalidMoney() { + //given + final Products products = new Products(Arrays.asList(new Product("abc", 20, 20), new Product("def", 10, 10))); + + //when + final boolean purchasable = products.isPurchasable(new UserMoney(5)); + + //then + assertThat(purchasable).isFalse(); + } + + @Test + @DisplayName("제품의 가격보다 큰 UserMoney의 경우 True를 반환한다") + void isPurchasableWithValidMoney() { + //given + final Products products = new Products(Arrays.asList(new Product("abc", 20, 20), new Product("def", 10, 10))); + + //when + final boolean purchasable = products.isPurchasable(new UserMoney(5)); + + //then + assertThat(purchasable).isFalse(); + } + + @Nested + class PurchaseProduct { + @Test + @DisplayName("제품을 살 때 살 수 있는 제품이 없으면 FINISHED를 반환한다") + void purchaseProductWhenNotPurchasable() { + //given + final Product product = new Product("abc", 2000, 20); + final Products products = new Products(Arrays.asList(product, new Product("def", 1000, 10))); + + //when + final BuyStatus buyStatus = products.purchaseProduct(new UserMoney(100), product); + + //then + assertThat(buyStatus).isEqualTo(BuyStatus.FINISHED); + } + + @Test + @DisplayName("제품을 살 때 살 수 있는 제품이 남아있으면 CONTINUE를 반환한다") + void purchaseProductWhenPurchasable() { + //given + final Product product = new Product("abc", 20, 10); + final Products products = new Products(Arrays.asList(product, new Product("def", 10, 10))); + + //when + final BuyStatus buyStatus = products.purchaseProduct(new UserMoney(1000), product); + + //then + assertThat(buyStatus).isEqualTo(BuyStatus.CONTINUE); + } + + @Test + @DisplayName("제품을 산 후 살 수 있는 제품이 없으면 FINISHED를 반환한다") + void purchaseProductWhenPurchasableAndFinish() { + //given + final Product product1 = new Product("abc", 20, 1); + final Product product2 = new Product("def", 10, 1); + final Products products = new Products(Arrays.asList(product1, product2)); + + //when + final BuyStatus buyStatus1 = products.purchaseProduct(new UserMoney(30), product1); + final BuyStatus buyStatus2 = products.purchaseProduct(new UserMoney(30), product2); + + //then + assertThat(buyStatus1).isEqualTo(BuyStatus.CONTINUE); + assertThat(buyStatus2).isEqualTo(BuyStatus.FINISHED); + } + } +} diff --git a/src/test/java/vendingmachine/domain/UserMoneyTest.java b/src/test/java/vendingmachine/domain/UserMoneyTest.java new file mode 100644 index 000000000..a3aeba885 --- /dev/null +++ b/src/test/java/vendingmachine/domain/UserMoneyTest.java @@ -0,0 +1,24 @@ +package vendingmachine.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("유저 잔액 도메인에서") +class UserMoneyTest { + + @Test + @DisplayName("amount를 음수로 생성시 예외를 던진다") + void constructor() { + assertThatThrownBy(() -> new UserMoney(-100)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("잔액보다 큰 금액의 상품 구매시 예외를 던진다") + void decrease() { + assertThatThrownBy(() -> new UserMoney(10).decrease(new Product("123", 100, 123))) + .isInstanceOf(IllegalStateException.class); + } +} diff --git a/src/test/java/vendingmachine/domain/VendingMachineMoneyTest.java b/src/test/java/vendingmachine/domain/VendingMachineMoneyTest.java new file mode 100644 index 000000000..51ca68695 --- /dev/null +++ b/src/test/java/vendingmachine/domain/VendingMachineMoneyTest.java @@ -0,0 +1,24 @@ +package vendingmachine.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("자판기가 가지는 거스름돈 총 합 도메인에서") +class VendingMachineMoneyTest { + + @Test + @DisplayName("최소 단위로 나눠떨어지지 않는 경우 예외를 던진다") + void constructor() { + assertThatThrownBy(() -> new VendingMachineMoney(5)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("") + void minusValue() { + assertThatThrownBy(() -> new VendingMachineMoney(50).minusValue(60)) + .isInstanceOf(IllegalStateException.class); + } +} diff --git a/src/test/java/vendingmachine/io/InputValidatorTest.java b/src/test/java/vendingmachine/io/InputValidatorTest.java new file mode 100644 index 000000000..56f6e7330 --- /dev/null +++ b/src/test/java/vendingmachine/io/InputValidatorTest.java @@ -0,0 +1,26 @@ +package vendingmachine.io; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("입력값 검증 클래스에서") +class InputValidatorTest { + + private InputValidator inputValidator = new InputValidator(); + + @Test + @DisplayName("숫자가 아닌 입력이 들어온 경우 예외를 던진다") + void validateNumeric() { + assertThatThrownBy(() -> inputValidator.validateNumeric("123abc")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("입력 형태와 맞지 않는 입력이 들어온 경우 예외를 던진다") + void validateProducts() { + assertThatThrownBy(() -> inputValidator.validateProducts("123abc")) + .isInstanceOf(IllegalArgumentException.class); + } +}