diff --git a/src/main/java/vendingmachine/Application.java b/src/main/java/vendingmachine/Application.java index 9d3be447b..21a8ed1e1 100644 --- a/src/main/java/vendingmachine/Application.java +++ b/src/main/java/vendingmachine/Application.java @@ -1,7 +1,16 @@ package vendingmachine; +import vendingmachine.coin.generator.RandomCoinGenerator; +import vendingmachine.controller.InitialController; +import vendingmachine.controller.PurchaseController; + public class Application { + private static final InitialController initialController = new InitialController(); + private static final PurchaseController purchaseController = new PurchaseController(); + public static void main(String[] args) { - // TODO: 프로그램 구현 + VendingMachine vendingMachine = initialController.create(new RandomCoinGenerator()); + purchaseController.purchase(vendingMachine); + } } 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/Credit.java b/src/main/java/vendingmachine/Credit.java new file mode 100644 index 000000000..615efc772 --- /dev/null +++ b/src/main/java/vendingmachine/Credit.java @@ -0,0 +1,35 @@ +package vendingmachine; + +import vendingmachine.exception.VendingMachineException; +import vendingmachine.menu.Menu; + +public class Credit { + private int money; + + public Credit(int money){ + validateMoney(money); + this.money = money; + } + + private void validateMoney(int money) { + if(money < 100 || money % 10 != 0){ + throw VendingMachineException.INVALID_MONEY_VALUE.makeException(); + } + } + + + public boolean canPurchase(int price){ + return money >= price; + } + + public void purchase(Menu menu){ + if(menu.getPrice() > money){ + throw VendingMachineException.CANT_PURCHASE.makeException(); + } + money -= menu.getPrice(); + } + + public int getMoney() { + return money; + } +} diff --git a/src/main/java/vendingmachine/VendingMachine.java b/src/main/java/vendingmachine/VendingMachine.java new file mode 100644 index 000000000..3e47d3127 --- /dev/null +++ b/src/main/java/vendingmachine/VendingMachine.java @@ -0,0 +1,41 @@ +package vendingmachine; + +import java.util.Map; +import vendingmachine.coin.Coin; +import vendingmachine.coin.Coins; +import vendingmachine.menu.Menu; +import vendingmachine.menu.Menus; + +public class VendingMachine { + private final Menus menus; + private final Credit credit; + private final Coins coins; + + public VendingMachine(Menus menus, Credit credit, Coins coins) { + this.menus = menus; + this.credit = credit; + this.coins = coins; + } + + public boolean isSellable(){ + if(menus.isSoldOut()){ + return false; + } + int minPrice = menus.getMinPrice(); + return credit.canPurchase(minPrice); + } + + public int getRemainMoney(){ + return credit.getMoney(); + } + + public void purchase(String menuName){ + Menu menu = menus.getMenu(menuName); + credit.purchase(menu); + menus.purchase(menu); + } + + public Map giveChange(){ + return coins.giveChange(credit.getMoney()); + } +} diff --git a/src/main/java/vendingmachine/coin/Coin.java b/src/main/java/vendingmachine/coin/Coin.java new file mode 100644 index 000000000..d92b30698 --- /dev/null +++ b/src/main/java/vendingmachine/coin/Coin.java @@ -0,0 +1,30 @@ +package vendingmachine.coin; + +import java.util.Arrays; +import vendingmachine.exception.VendingMachineException; + +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 getCoins(int amount){ + return Arrays.stream(values()) + .filter(coin -> coin.amount == amount) + .findFirst() + .orElseThrow(VendingMachineException.INVALID_COIN_AMOUNT::makeException); + } + + + public int getAmount() { + return amount; + } +} diff --git a/src/main/java/vendingmachine/coin/Coins.java b/src/main/java/vendingmachine/coin/Coins.java new file mode 100644 index 000000000..f96cd7efe --- /dev/null +++ b/src/main/java/vendingmachine/coin/Coins.java @@ -0,0 +1,40 @@ +package vendingmachine.coin; + +import java.util.EnumMap; +import java.util.Map; + +public class Coins { + private final Map coins; + + public Coins(Map coins){ + this.coins = coins; + } + + public int getCounts(Coin coin){ + return coins.get(coin); + } + + public Map giveChange(int changeMoney){ + Map changes = new EnumMap<>(Coin.class); + for(Coin coin: Coin.values()){ + if(coin.getAmount() > changeMoney){ + changes.remove(coin); + continue; + } + int count = getCount(changeMoney, coin); + changes.put(coin, count); + changeMoney -= coin.getAmount() * count; + } + + return changes; + } + + private int getCount(int changeMoney, Coin coin) { + int count = coins.get(coin); + while(count * coin.getAmount() > changeMoney){ + count--; + } + return count; + } + +} diff --git a/src/main/java/vendingmachine/coin/generator/CoinGenerator.java b/src/main/java/vendingmachine/coin/generator/CoinGenerator.java new file mode 100644 index 000000000..8841d559e --- /dev/null +++ b/src/main/java/vendingmachine/coin/generator/CoinGenerator.java @@ -0,0 +1,8 @@ +package vendingmachine.coin.generator; + +import java.util.Map; +import vendingmachine.coin.Coin; + +public interface CoinGenerator { + Map getCoins(int totalMoney); +} diff --git a/src/main/java/vendingmachine/coin/generator/RandomCoinGenerator.java b/src/main/java/vendingmachine/coin/generator/RandomCoinGenerator.java new file mode 100644 index 000000000..c617bb593 --- /dev/null +++ b/src/main/java/vendingmachine/coin/generator/RandomCoinGenerator.java @@ -0,0 +1,44 @@ +package vendingmachine.coin.generator; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import vendingmachine.coin.Coin; + +public class RandomCoinGenerator implements CoinGenerator{ + + @Override + public Map getCoins(int totalMoney) { + EnumMap coins = initCoins(); + List integers = initIntegers(); + while(totalMoney > 0){ + int amount = Randoms.pickNumberInList(integers); + if(amount > totalMoney){ + continue; + } + Coin coin = Coin.getCoins(amount); + coins.put(coin, coins.get(coin) + 1); + totalMoney -= amount; + } + return coins; + } + + private static EnumMap initCoins() { + EnumMap coins = new EnumMap<>(Coin.class); + Arrays.stream(Coin.values()) + .forEach(coin -> coins.put(coin, 0)); + return coins; + } + + private static List initIntegers() { + List integers = new ArrayList<>(); + integers.add(500); + integers.add(100); + integers.add(50); + integers.add(10); + return integers; + } +} diff --git a/src/main/java/vendingmachine/controller/InitialController.java b/src/main/java/vendingmachine/controller/InitialController.java new file mode 100644 index 000000000..592d55883 --- /dev/null +++ b/src/main/java/vendingmachine/controller/InitialController.java @@ -0,0 +1,56 @@ +package vendingmachine.controller; + +import java.util.List; +import java.util.function.Supplier; +import vendingmachine.Credit; +import vendingmachine.VendingMachine; +import vendingmachine.coin.Coins; +import vendingmachine.coin.generator.CoinGenerator; +import vendingmachine.exception.RetryExceptionHandler; +import vendingmachine.exception.VendingMachineException; +import vendingmachine.menu.Menus; +import vendingmachine.view.InputView; +import vendingmachine.view.OutputView; + +public class InitialController { + public VendingMachine create(CoinGenerator generator){ + Coins coins = get(() -> makeCoins(generator)); + Menus menus = get(this::makeMenus); + Credit credit = get(this::getCredit); + return new VendingMachine(menus, credit, coins); + } + + private Coins makeCoins(CoinGenerator generator) { + int coinMoney = getCoinMoney(); + Coins coins = new Coins(generator.getCoins(coinMoney)); + OutputView.printCoins(coins); + return coins; + } + + private int getCoinMoney() { + int coinMoney = InputView.getCoinMoney(); + validateMoney(coinMoney); + return coinMoney; + } + + private void validateMoney(int coinMoney) { + if(coinMoney < 0 || coinMoney % 10 != 0){ + throw VendingMachineException.INVALID_MONEY_VALUE.makeException(); + } + } + + private Menus makeMenus() { + List menus = InputView.getMenus(); + + return new Menus(menus); + } + + private Credit getCredit(){ + int initMoney = InputView.getInitMoney(); + return new Credit(initMoney); + } + + private T get(Supplier supplier){ + return RetryExceptionHandler.get(supplier); + } +} diff --git a/src/main/java/vendingmachine/controller/PurchaseController.java b/src/main/java/vendingmachine/controller/PurchaseController.java new file mode 100644 index 000000000..3c9022110 --- /dev/null +++ b/src/main/java/vendingmachine/controller/PurchaseController.java @@ -0,0 +1,36 @@ +package vendingmachine.controller; + +import java.util.Map; +import vendingmachine.VendingMachine; +import vendingmachine.coin.Coin; +import vendingmachine.exception.RetryExceptionHandler; +import vendingmachine.view.InputView; +import vendingmachine.view.OutputView; + +public class PurchaseController { + public void purchase(VendingMachine machine) { + while (machine.isSellable()) { + RetryExceptionHandler.run(() ->purchaseMenu(machine)); + } + printResult(machine); + } + + private void printResult(VendingMachine machine) { + printRemainMoney(machine); + Map coinIntegerMap = machine.giveChange(); + OutputView.printChange(coinIntegerMap); + } + + private static void purchaseMenu(VendingMachine machine) { + printRemainMoney(machine); + String menuName = InputView.getMenuName(); + machine.purchase(menuName); + } + + private static void printRemainMoney(VendingMachine machine) { + int remainMoney = machine.getRemainMoney(); + OutputView.printRemainMoney(remainMoney); + } + + +} diff --git a/src/main/java/vendingmachine/exception/RetryExceptionHandler.java b/src/main/java/vendingmachine/exception/RetryExceptionHandler.java new file mode 100644 index 000000000..9cb6baefe --- /dev/null +++ b/src/main/java/vendingmachine/exception/RetryExceptionHandler.java @@ -0,0 +1,33 @@ +package vendingmachine.exception; + +import java.util.function.Supplier; +import vendingmachine.view.io.Printer; + +public class RetryExceptionHandler { + private RetryExceptionHandler(){} + + public static T get(Supplier supplier){ + while(true) { + try{ + return supplier.get(); + } catch (IllegalArgumentException e){ + Printer.printMessage(e.getMessage()); + } finally { + Printer.printMessage(""); + } + } + } + + public static void run(Runnable runnable){ + while(true) { + try{ + runnable.run(); + return; + } catch (IllegalArgumentException e){ + Printer.printMessage(e.getMessage()); + } finally { + Printer.printMessage(""); + } + } + } +} diff --git a/src/main/java/vendingmachine/exception/VendingMachineException.java b/src/main/java/vendingmachine/exception/VendingMachineException.java new file mode 100644 index 000000000..4f0fa2985 --- /dev/null +++ b/src/main/java/vendingmachine/exception/VendingMachineException.java @@ -0,0 +1,32 @@ +package vendingmachine.exception; + +public enum VendingMachineException { + INVALID_NUMBER_FORMAT("숫자를 입력해 주세요"), + INVALID_MONEY_VALUE("입력 가격이 잘못되었습니다."), + INVALID_STRING_FORMAT("입력 형식이 잘못되었습니다."), + END_WITH_DELIMITER("입력의 마지막이 구분자로 끝났습니다."), + + INVALID_COIN_AMOUNT("해당 금액의 동전을 찾을 수 없습니다."), + EMPTY_MENU_LIST("메뉴가 입력되지 않았습니다."), + NO_MENU_FOUNDED("해당 메뉴를 찾을 수 없습니다."), + + CANT_PURCHASE("구매할 수 없는 상품입니다."), + MENU_AMOUNT_MUST_POSITIVE("메뉴의 수량은 0보다 큰 값이어야 합니다."), + NO_INPUT_FOUNDED("입력이 존재하지 않습니다."), + ; + + private static final String PREFIX = "[ERROR] "; + private final String messsage; + + VendingMachineException(String messsage) { + this.messsage = messsage; + } + + public String getMesssage() { + return PREFIX + messsage; + } + + public IllegalArgumentException makeException(){ + return new IllegalArgumentException(getMesssage()); + } +} diff --git a/src/main/java/vendingmachine/menu/Menu.java b/src/main/java/vendingmachine/menu/Menu.java new file mode 100644 index 000000000..734ac5c28 --- /dev/null +++ b/src/main/java/vendingmachine/menu/Menu.java @@ -0,0 +1,33 @@ +package vendingmachine.menu; + +import vendingmachine.exception.VendingMachineException; + +public class Menu { + private final String name; + private final int price; + + private Menu(String name, int price) { + this.name = name; + this.price = price; + } + + public static Menu of(String menuName, int price){ + validatePrice(price); + return new Menu(menuName, price); + } + + private static void validatePrice(int price) { + if(price < 100 || price % 10 != 0){ + throw VendingMachineException.INVALID_MONEY_VALUE.makeException(); + } + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + +} diff --git a/src/main/java/vendingmachine/menu/Menus.java b/src/main/java/vendingmachine/menu/Menus.java new file mode 100644 index 000000000..3f184f316 --- /dev/null +++ b/src/main/java/vendingmachine/menu/Menus.java @@ -0,0 +1,69 @@ +package vendingmachine.menu; + +import java.util.HashMap; +import java.util.List; +import vendingmachine.exception.VendingMachineException; + +public class Menus { + HashMap menus = new HashMap<>(); + + public Menus(List menusInputs) { + validateListNotEmpty(menusInputs); + menusInputs.stream() + .map(input -> input.substring(1, input.length() - 1).split(",")) + .forEach(this::putMenu); + } + + private void putMenu(String[] inputs) { + String menuName = inputs[0]; + int menuPrice = Integer.parseInt(inputs[1]); + int menuAmount = Integer.parseInt(inputs[2]); + validateAmount(menuAmount); + + menus.put(Menu.of(menuName, menuPrice), + menuAmount); + } + + private void validateAmount(int menuAmount) { + if (menuAmount < 1) { + throw VendingMachineException.MENU_AMOUNT_MUST_POSITIVE.makeException(); + } + } + + private void validateListNotEmpty(List menusInputs) { + if (menusInputs.isEmpty()) { + throw VendingMachineException.EMPTY_MENU_LIST.makeException(); + } + } + + public Menu getMenu(String menuName) { + return menus.keySet().stream() + .filter(key -> key.getName().equals(menuName)) + .findFirst() + .orElseThrow(VendingMachineException.NO_MENU_FOUNDED::makeException); + } + + public boolean isSoldOut() { + return menus.isEmpty(); + } + + public int getMinPrice() { + return menus.keySet().stream() + .mapToInt(Menu::getPrice) + .min() + .getAsInt(); + } + + public void purchase(Menu menu) { + Integer count = menus.get(menu); + if (count <= 0) { + throw VendingMachineException.CANT_PURCHASE.makeException(); + } + if (count == 1) { + menus.remove(menu); + return; + } + + menus.put(menu, menus.get(menu) - 1); + } +} diff --git a/src/main/java/vendingmachine/view/InputView.java b/src/main/java/vendingmachine/view/InputView.java new file mode 100644 index 000000000..f885c27e4 --- /dev/null +++ b/src/main/java/vendingmachine/view/InputView.java @@ -0,0 +1,45 @@ +package vendingmachine.view; + +import java.util.List; +import java.util.regex.Pattern; +import vendingmachine.exception.VendingMachineException; +import vendingmachine.view.io.Printer; +import vendingmachine.view.io.Reader; + +public class InputView { + private static final Pattern menuInputPattern = Pattern.compile("\\[\\W*,\\d*,\\d*\\]"); + private static final Pattern menuNamePattern = Pattern.compile("\\W*"); + private InputView(){} + + public static int getCoinMoney(){ + Printer.printMessage("자판기가 보유하고 있는 금액을 입력해 주세요."); + return Reader.getInteger(); + } + + public static List getMenus(){ + Printer.printMessage("상품명과 가격, 수량을 입력해 주세요."); + List menuInputs = Reader.getStrings(); + menuInputs.forEach(InputView::validateMenuPattern); + return menuInputs; + } + + private static void validateMenuPattern(String input){ + if(!menuInputPattern.matcher(input).matches()){ + throw VendingMachineException.INVALID_STRING_FORMAT.makeException(); + } + } + + public static int getInitMoney(){ + Printer.printMessage("투입 금액을 입력해 주세요."); + return Reader.getInteger(); + } + + public static String getMenuName(){ + Printer.printMessage("구매할 상품명을 입력해 주세요."); + String menuName = Reader.getString(); + if(!menuNamePattern.matcher(menuName).matches()){ + throw VendingMachineException.INVALID_STRING_FORMAT.makeException(); + } + return menuName; + } +} diff --git a/src/main/java/vendingmachine/view/OutputView.java b/src/main/java/vendingmachine/view/OutputView.java new file mode 100644 index 000000000..38f85723a --- /dev/null +++ b/src/main/java/vendingmachine/view/OutputView.java @@ -0,0 +1,31 @@ +package vendingmachine.view; + +import java.util.Arrays; +import java.util.Map; +import vendingmachine.coin.Coin; +import vendingmachine.coin.Coins; +import vendingmachine.view.io.Printer; + +public class OutputView { + private OutputView(){} + + public static void printCoins(Coins coins) { + Printer.printMessage("자판기가 보유한 동전"); + Arrays.stream(Coin.values()) + .forEach(coin -> + Printer.printUsingFormat("%d원 - %d개", coin.getAmount(), coins.getCounts(coin))); + } + + public static void printRemainMoney(int remainMoney) { + Printer.printUsingFormat("투입 금액: %d원", remainMoney); + } + + public static void printChange(Map changes){ + Printer.printMessage("잔돈"); + Arrays.stream(Coin.values()) + .filter(changes::containsKey) + .forEach(coin -> + Printer.printUsingFormat("%d원 - %d개", + coin.getAmount(), changes.get(coin))); + } +} diff --git a/src/main/java/vendingmachine/view/io/Printer.java b/src/main/java/vendingmachine/view/io/Printer.java new file mode 100644 index 000000000..3c08ca444 --- /dev/null +++ b/src/main/java/vendingmachine/view/io/Printer.java @@ -0,0 +1,14 @@ +package vendingmachine.view.io; + +public class Printer { + private Printer(){} + + public static void printMessage(String message){ + System.out.println(message); + } + + public static void printUsingFormat(String format, Object... args){ + System.out.printf(format, args); + System.out.println(); + } +} diff --git a/src/main/java/vendingmachine/view/io/Reader.java b/src/main/java/vendingmachine/view/io/Reader.java new file mode 100644 index 000000000..1068d0aba --- /dev/null +++ b/src/main/java/vendingmachine/view/io/Reader.java @@ -0,0 +1,56 @@ +package vendingmachine.view.io; + +import camp.nextstep.edu.missionutils.Console; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import vendingmachine.exception.VendingMachineException; + +public class Reader { + private Reader() { + } + + public static int getInteger() { + String input = Console.readLine(); + return parseInteger(input); + } + + private static int parseInteger(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw VendingMachineException.INVALID_NUMBER_FORMAT.makeException(); + } + } + + public static String getString() { + try { + String input = Console.readLine(); + validateInput(input); + return input; + } catch (NoSuchElementException e) { + throw VendingMachineException.NO_INPUT_FOUNDED.makeException(); + } + } + + private static void validateInput(String input) { + if (input == null || input.isEmpty()) { + throw VendingMachineException.INVALID_STRING_FORMAT.makeException(); + } + } + + public static List getStrings() { + String input = Console.readLine(); + validateNotEndWithDelimiter(input); + return Arrays.stream(input.split(";")) + .collect(Collectors.toList()); + } + + private static void validateNotEndWithDelimiter(String input) { + if (input.charAt(input.length() - 1) == ';') { + throw VendingMachineException.END_WITH_DELIMITER.makeException(); + } + } + +} diff --git a/src/test/java/vendingmachine/menu/MenuTest.java b/src/test/java/vendingmachine/menu/MenuTest.java new file mode 100644 index 000000000..be76b9c4d --- /dev/null +++ b/src/test/java/vendingmachine/menu/MenuTest.java @@ -0,0 +1,35 @@ +package vendingmachine.menu; + +import static org.junit.jupiter.api.Assertions.*; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import vendingmachine.exception.VendingMachineException; + +class MenuTest { + + @Nested + @DisplayName("객체 생성 테스트") + class 객체_생성_테스트{ + + @ParameterizedTest(name = "{0}원의 메뉴는 정상적으로 생성된다") + @DisplayName("[SUCCESS] 정상적으로 메뉴가 생성된다.") + @ValueSource(ints = {100, 110, 1500, 4300}) + void 성공_테스트(int price){ + Assertions.assertThatNoException() + .isThrownBy(() -> Menu.of("콜라", price)); + } + + @ParameterizedTest(name = "{0}원의 메뉴를 입력하면 예외가 발생한다") + @DisplayName("[EXCEPTION] 메뉴 생성시 예외가 발생한다..") + @ValueSource(ints = {1, 90, 115, 0, -1140}) + void 실패_테스트(int price){ + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> Menu.of("콜라", price)) + .withMessage(VendingMachineException.INVALID_MONEY_VALUE.getMesssage()); + } + } +} \ No newline at end of file