diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..9cdc0fe4e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# 미션 - 자판기 + +--- +### 구현할 기능 목록 + +👉 자판기 금액 +- [X] 자판기가 보유한 금액을 생성한다. + +👉 자판기 금액 +- [ ] 상품 리스트를 만든다. + +👉 잔돈 반환 +- [ ] 사용자의 금액이 상품의 최저 가격보다 적으면 잔돈을 반환한다. +- [ ] 모든 상품이 소진된 경우 잔돈을 반환한다. +- [ ] 지폐를 잔돈으로 반환하지 않는다. +- [ ] 잔돈 반환시 현재 보유한 최소 개수의 동전으로 잔돈을 반환한다. +- [ ] 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다. + + +👉 예외 사항 + - [X] 금액은 숫자만 입력되어야한다. + - [X] 상품가격은 100원부터 시작한다. + - [X] 상품가격은 10원으로 나누어 떨어져야한다. + - [X] 입력 양식에 어긋나면 예외처리 + - [X] 개별 상품은 [] 대괄호로 묶인다. + - [X] 상품들은 ; 으로 구분된다. diff --git a/src/main/java/vendingmachine/Application.java b/src/main/java/vendingmachine/Application.java index 9d3be447b..2af791c72 100644 --- a/src/main/java/vendingmachine/Application.java +++ b/src/main/java/vendingmachine/Application.java @@ -1,7 +1,15 @@ package vendingmachine; +import vendingmachine.controller.VendingMachineController; + public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 + VendingMachineController vendingMachineController = new VendingMachineController(); + try{ + vendingMachineController.gameStart(); + }catch (Exception e){ + System.out.println(e.getMessage()); + } } } 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/controller/VendingMachineController.java b/src/main/java/vendingmachine/controller/VendingMachineController.java new file mode 100644 index 000000000..0475e8f0f --- /dev/null +++ b/src/main/java/vendingmachine/controller/VendingMachineController.java @@ -0,0 +1,54 @@ +package vendingmachine.controller; + +import vendingmachine.vendingmachine.Coin; +import vendingmachine.goods.GoodsManager; +import vendingmachine.vendingmachine.CoinMaker; +import vendingmachine.view.InputView; +import vendingmachine.view.OutputView; + +public class VendingMachineController { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + CoinMaker coinMaker = new CoinMaker(); + GoodsManager goodsManager; + + public void gameStart() { + String money = inputView.inputVendingMoney(); + makeVendingMachineCoin(money); + } + + private void makeVendingMachineCoin(String money) { + coinMaker.randomCoin(money); + outputView.vendingMachineCoin(Coin.makeCoinList()); + makeGoodsCataLog(); + } + + private void makeGoodsCataLog() { + String goodsCataLog = inputView.inputGoodsPriceAmount(); + goodsManager = new GoodsManager(goodsCataLog); + InputUserMoney(); + } + + private void InputUserMoney() { + int money = Integer.parseInt(inputView.inputUserMoney()); + outputView.remainMoney(money); + purchaseGoods(money); + } + + + private void purchaseGoods(int money) { + while (goodsManager.validMinPurchase(money)) { + String goods = inputView.inputPurchaseGoods(); + if (!goodsManager.validPurchase(goods, money)) break; + + money = goodsManager.purchaseGoods(goods, money); + outputView.remainMoney(money); + } + returnCoin(money); + } + + private void returnCoin(int money) { + if (money == 0) return; + outputView.returnCoinList(coinMaker.remainCoinList(money)); + } +} diff --git a/src/main/java/vendingmachine/exception/VendingMachineValidator.java b/src/main/java/vendingmachine/exception/VendingMachineValidator.java new file mode 100644 index 000000000..b5e39d188 --- /dev/null +++ b/src/main/java/vendingmachine/exception/VendingMachineValidator.java @@ -0,0 +1,48 @@ +package vendingmachine.exception; + +import vendingmachine.goods.Goods; + +import java.util.HashMap; + +public class VendingMachineValidator { + + public void allOfValidInput(String money){ + validMoneyInputForm(money); + validMoneyType(money); + + } + + private void validMoneyInputForm(String money) { + final String ONLY_NUMBER = "^[0-9]*$"; + + if (!money.matches(ONLY_NUMBER)) + throw new IllegalArgumentException("[ERROR] 금액은 숫자만 입력하세요."); + } + + private void validMoneyType(String money) { + if (Integer.parseInt(money) < 10) + throw new IllegalArgumentException("[ERROR] 자판기 금액은 10원부터 시작입니다."); + if (Integer.parseInt(money) % 10 !=0) + throw new IllegalArgumentException("[ERROR] 자판기 금액은 10원 단위로 입력해주세요."); + } + + public void validGoodsName(String goods, HashMap goodsInfo){ + if(goodsInfo.get(goods)==null) + throw new IllegalArgumentException("[ERROR] 상품 리스트에 존재하는 상품만 입력하세요."); + } + + public void validGoodsInputForm(String goods) { + final String GOODS_INPUT_FORM = "\\[[0-9가-힇A-Za-z]+,?[0-9]*,?[0-9]*\\];?"; + + if (goods.replaceAll(GOODS_INPUT_FORM, "").length() != 0) + throw new IllegalArgumentException("[ERROR] 상품명, 가격, 수량은 쉼표로, 개별 상품은 대괄호([])로 묶어주세요."); + } + + public void validGoodsMoneyType(String money) { + int subMoney = Integer.parseInt(money); + if (subMoney < 100) + throw new IllegalArgumentException("[ERROR] 상품 가격은 100원부터 시작합니다."); + if (subMoney % 10 != 0) + throw new IllegalArgumentException("[ERROR] 상품 가격은 10원 단위로 입력하세요."); + } +} diff --git a/src/main/java/vendingmachine/goods/Goods.java b/src/main/java/vendingmachine/goods/Goods.java new file mode 100644 index 000000000..4449853ac --- /dev/null +++ b/src/main/java/vendingmachine/goods/Goods.java @@ -0,0 +1,28 @@ +package vendingmachine.goods; + +public class Goods { + private int price; + private int amount; + + public Goods(String[] goodsInformation){ + makeGoodsInfo(goodsInformation); + } + + public int getAmount() { + return amount; + } + + public int getPrice() { + return price; + } + + public void reduceAmount() { + amount--; + } + + void makeGoodsInfo(String[] goodsInformation) { + price = Integer.parseInt(goodsInformation[1]); + amount = Integer.parseInt(goodsInformation[2]); + } +} + diff --git a/src/main/java/vendingmachine/goods/GoodsManager.java b/src/main/java/vendingmachine/goods/GoodsManager.java new file mode 100644 index 000000000..63adfbcaf --- /dev/null +++ b/src/main/java/vendingmachine/goods/GoodsManager.java @@ -0,0 +1,54 @@ +package vendingmachine.goods; + +import vendingmachine.exception.VendingMachineValidator; + +import java.util.HashMap; + +public class GoodsManager { + VendingMachineValidator vendingMachineValidator = new VendingMachineValidator(); + private final HashMap goodsInfo = new HashMap<>(); + private int minPrice = Integer.MAX_VALUE; + + + public GoodsManager(String goodsList){ + makeGoodsInformation(goodsList); + minPriceGoodsList(); + } + + private void makeGoodsInformation(String goodsList) { + String[] itemList = goodsList.split(";"); + + for (String item : itemList) { + String[] goodsInformation = item.replaceAll("[\\[\\]]","").split(","); + + vendingMachineValidator.validGoodsMoneyType(goodsInformation[1]); + + Goods goods = new Goods(goodsInformation); + goodsInfo.put(goodsInformation[0],goods); + } + } + + private void minPriceGoodsList() { + for (Goods g : goodsInfo.values()) + if (g.getPrice() < minPrice) minPrice = g.getPrice(); + } + + public boolean validPurchase(String purchaseGoods, int money) { + vendingMachineValidator.validGoodsName(purchaseGoods,goodsInfo); + Goods goods = goodsInfo.get(purchaseGoods); + return goods.getPrice() <= money && goods.getAmount() != 0; + } + + public boolean validMinPurchase(int money) { + return money >= minPrice; + } + + public int purchaseGoods(String purchaseGoods, int money) { + Goods goods = goodsInfo.get(purchaseGoods); + if (goods.getAmount() != 0) + goods.reduceAmount(); + return money - goods.getPrice(); + } + + +} diff --git a/src/main/java/vendingmachine/vendingmachine/Coin.java b/src/main/java/vendingmachine/vendingmachine/Coin.java new file mode 100644 index 000000000..cae2717dc --- /dev/null +++ b/src/main/java/vendingmachine/vendingmachine/Coin.java @@ -0,0 +1,44 @@ +package vendingmachine.vendingmachine; + +import java.util.Arrays; + +public enum Coin { + COIN_500(500, 0), + COIN_100(100, 0), + COIN_50(50, 0), + COIN_10(10, 0); + + private final int amount; + private int count; + + Coin(final int amount, final int count) { + this.amount = amount; + this.count = count; + } + + + // 추가 기능 구현 + public int getCount() { + return count; + } + + public int getAmount() { + return amount; + } + + public void addCoin() { + count++; + } + + public static Coin getCoin(int amount) { + return Arrays.stream(values()).filter(a -> a.getAmount() == amount).findAny().orElseGet(() -> null); + } + + public static StringBuilder makeCoinList() { + StringBuilder sb = new StringBuilder(); + Arrays.stream(values()).forEach(coin -> sb.append(coin.getAmount()).append("원 - ").append(coin.getCount()).append("개").append("\n")); + return sb; + } + + +} diff --git a/src/main/java/vendingmachine/vendingmachine/CoinMaker.java b/src/main/java/vendingmachine/vendingmachine/CoinMaker.java new file mode 100644 index 000000000..e53f8b76b --- /dev/null +++ b/src/main/java/vendingmachine/vendingmachine/CoinMaker.java @@ -0,0 +1,43 @@ +package vendingmachine.vendingmachine; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.*; + +public class CoinMaker { + + public void randomCoin(String money) { + int vendingMoney = Integer.parseInt(money); + List vendingMoneyList = Arrays.asList(500, 100, 50, 10); + + while (vendingMoney != 0) { + int num = Randoms.pickNumberInList(vendingMoneyList); + if (vendingMoney - num >= 0) { + vendingMoney -= num; + Coin.getCoin(num).addCoin(); + } + } + + } + + public HashMap remainCoinList(int money) { + HashMap coinList = new LinkedHashMap<>(); + + for (Coin coin : Coin.values()) { + if (money <= 0) break; + int require = money / coin.getAmount(); + + if (require > coin.getCount()) { + money -= (coin.getCount() * coin.getAmount()); + coinList.put(coin.getAmount(), coin.getCount()); + continue; + } + + money -= (require * coin.getAmount()); + coinList.put(coin.getAmount(), require); + } + return coinList; + } + + +} \ No newline at end of file diff --git a/src/main/java/vendingmachine/view/GameMessage.java b/src/main/java/vendingmachine/view/GameMessage.java new file mode 100644 index 000000000..e75a18505 --- /dev/null +++ b/src/main/java/vendingmachine/view/GameMessage.java @@ -0,0 +1,12 @@ +package vendingmachine.view; + +public class GameMessage { + public static final String INPUT_VENDINGMACHINE_MONEY = "자판기가 보유하고 있는 금액을 입력해 주세요."; + public static final String INPUT_GOODS_PRICE_AMOUNT = "상품명과 가격, 수량을 입력해 주세요."; + public static final String INPUT_USER_MONEY = "투입 금액을 입력해 주세요."; + public static final String INPUT_PURCHASE_GOODS = "구매할 상품명을 입력해 주세요."; + + public static final String OUTPUT_USER_MONEY = "투입 금액: "; + public static final String OUTPUT_VENDINGMACHINE_MONEY = "자판기가 보유한 동전"; + public static final String OUTPUT_REMAIN_MONEY = "잔돈"; +} diff --git a/src/main/java/vendingmachine/view/InputView.java b/src/main/java/vendingmachine/view/InputView.java new file mode 100644 index 000000000..c2509c5dc --- /dev/null +++ b/src/main/java/vendingmachine/view/InputView.java @@ -0,0 +1,36 @@ +package vendingmachine.view; + +import camp.nextstep.edu.missionutils.Console; +import vendingmachine.exception.VendingMachineValidator; + +public class InputView { + VendingMachineValidator vendingMachineValidator = new VendingMachineValidator(); + + public String inputVendingMoney(){ + System.out.println(GameMessage.INPUT_VENDINGMACHINE_MONEY); + String vendingMoney = Console.readLine(); + vendingMachineValidator.allOfValidInput(vendingMoney); + return vendingMoney; + } + + public String inputGoodsPriceAmount(){ + System.out.println(GameMessage.INPUT_GOODS_PRICE_AMOUNT); + String goods = Console.readLine(); + vendingMachineValidator.validGoodsInputForm(goods); + return goods; + } + + public String inputUserMoney(){ + System.out.println(GameMessage.INPUT_USER_MONEY); + String userMoney = Console.readLine(); + vendingMachineValidator.allOfValidInput(userMoney); + return userMoney; + } + + public String inputPurchaseGoods(){ + System.out.println(GameMessage.INPUT_PURCHASE_GOODS); + String goods = Console.readLine(); + return goods; + } + +} diff --git a/src/main/java/vendingmachine/view/OutputView.java b/src/main/java/vendingmachine/view/OutputView.java new file mode 100644 index 000000000..b94b88b96 --- /dev/null +++ b/src/main/java/vendingmachine/view/OutputView.java @@ -0,0 +1,23 @@ +package vendingmachine.view; + +import java.util.HashMap; +import java.util.List; + +public class OutputView { + + public void vendingMachineCoin(StringBuilder sb) { + System.out.println(GameMessage.OUTPUT_VENDINGMACHINE_MONEY); + System.out.println(sb); + } + + public void remainMoney(int price) { + System.out.println(GameMessage.OUTPUT_USER_MONEY + price + "원"); + } + + public void returnCoinList(HashMap coinList) { + System.out.println(GameMessage.OUTPUT_REMAIN_MONEY); + for (Integer key : coinList.keySet()) { + if (coinList.get(key) != 0) System.out.println(key + "원 - " + coinList.get(key) + "개"); + } + } +} diff --git a/src/test/java/vendingmachine/exception/VendingMachineValidatorTest.java b/src/test/java/vendingmachine/exception/VendingMachineValidatorTest.java new file mode 100644 index 000000000..8ef831b10 --- /dev/null +++ b/src/test/java/vendingmachine/exception/VendingMachineValidatorTest.java @@ -0,0 +1,65 @@ +package vendingmachine.exception; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class VendingMachineValidatorTest { + VendingMachineValidator vendingMachineValidator = new VendingMachineValidator(); + + @Test + @DisplayName("금액에 숫자가 입력되지 않으면 예외가 발생한다") + void validMoneyInputForm() { + //given + String money = "z1234"; + //then + assertThrows(IllegalArgumentException.class, () -> vendingMachineValidator.allOfValidInput(money)); //when + } + + @Test + @DisplayName("자판기 금액은 10 미만이고 10원 단위가 아니면 예외가 발생한다") + void validMoneyType() { + //given + String lowerMoney = "8"; + String notTenUnit = "12"; + + //when + IllegalArgumentException lowerMoneyException = assertThrows(IllegalArgumentException.class, + () -> vendingMachineValidator.allOfValidInput(lowerMoney)); + IllegalArgumentException notTenUnitException = assertThrows(IllegalArgumentException.class, + () -> vendingMachineValidator.allOfValidInput(notTenUnit)); + + //then + assertEquals("[ERROR] 자판기 금액은 10원부터 시작입니다.", lowerMoneyException.getMessage()); + assertEquals("[ERROR] 자판기 금액은 10원 단위로 입력해주세요.", notTenUnitException.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"[콜라,1500,20","[,1500,20]","[콜라,1500,20];[","[콜라,천오백원,20];"}) + @DisplayName("상품 등록 입력 형식이 주어진 양식에 어긋나면 예외가 발생한다") + void validGoodsInputForm(String goods) { + assertThrows(IllegalArgumentException.class, () -> vendingMachineValidator.validGoodsInputForm(goods)); //when + } + + + @Test + @DisplayName("상품가격이 100 미만 10원 단위가 아니면 예외가 발생한다") + void validGoodsMoneyType() { + //given + String lowerMoney = "90"; + String notTenUnit = "131"; + + //when + IllegalArgumentException lowerMoneyException = assertThrows(IllegalArgumentException.class, + () -> vendingMachineValidator.validGoodsMoneyType(lowerMoney)); + IllegalArgumentException notTenUnitException = assertThrows(IllegalArgumentException.class, + () -> vendingMachineValidator.validGoodsMoneyType(notTenUnit)); + + //then + assertEquals("[ERROR] 상품 가격은 100원부터 시작합니다.", lowerMoneyException.getMessage()); + assertEquals("[ERROR] 상품 가격은 10원 단위로 입력하세요.", notTenUnitException.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/vendingmachine/goods/GoodsManagerTest.java b/src/test/java/vendingmachine/goods/GoodsManagerTest.java new file mode 100644 index 000000000..433cdcc56 --- /dev/null +++ b/src/test/java/vendingmachine/goods/GoodsManagerTest.java @@ -0,0 +1,57 @@ +package vendingmachine.goods; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GoodsManagerTest { + GoodsManager goodsManager; + + @Test + @DisplayName("투입 금액이 상품 가격보다 적으면 false를 반환한다.") + void validPurchase() { + //given + goodsManager = new GoodsManager("[콜라,1500,20];[사이다,1000,10]"); + String Goods = "콜라"; + int money = 1490; + + //when + boolean actual = goodsManager.validPurchase(Goods,money); + + //then + assertFalse(actual); + + } + + @Test + @DisplayName("투입 가격이 상품의 최소 가격보다 작으면 false를 반환한다.") + void validMinPurchase() { + //given + goodsManager = new GoodsManager("[콜라,1500,20];[사이다,1000,10]"); + int money = 990; + + //when + boolean actual = goodsManager.validMinPurchase(money); + + //then + assertFalse(actual); + + } + + @Test + @DisplayName("상품 구매시, 투입가격에서 상품가격을 제외한만큼 반환된다.") + void purchaseGoods() { + //given + goodsManager = new GoodsManager("[콜라,1500,20];[사이다,1000,10]"); + int money = 1200; + String goods = "사이다"; + + //when + int actualPrice = goodsManager.purchaseGoods(goods,money); + + //then + assertEquals(200,actualPrice); + + } +}