From 296242bf6e479ceedd23dfe79f691e3a5e33c52e Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:30:20 +0900 Subject: [PATCH 01/10] =?UTF-8?q?docs=20:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EB=B3=84=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 114 ++++++++++++++++++------------------------------------ 1 file changed, 37 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 86699576c..9a5634ab9 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,3 @@ -# 미션 - 자판기 - -## 🔍 진행방식 - -- 미션은 **기능 요구사항, 프로그래밍 요구사항, 과제 진행 요구사항** 세 가지로 구성되어 있다. -- 세 개의 요구사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다. -- 기능 요구사항에 기재되지 않은 내용은 스스로 판단하여 구현한다. - -## ✉️ 미션 제출 방법 - -- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다. - - GitHub을 활용한 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고해 제출한다. -- GitHub에 미션을 제출한 후 [우아한테크코스 지원 플랫폼](https://apply.techcourse.co.kr) 에 접속하여 프리코스 과제를 제출한다. - - 자세한 방법은 [링크](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 를 참고한다. - - **Pull Request만 보내고, 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.** - -## ✔️ 과제 제출 전 체크리스트 - 0점 방지 - -- 터미널에서 `java -version`을 실행해 자바 8인지 확인한다. 또는 Eclipse, IntelliJ IDEA와 같은 IDE의 자바 8로 실행하는지 확인한다. -- 터미널에서 맥 또는 리눅스 사용자의 경우 `./gradlew clean test`, 윈도우 사용자의 경우 `gradlew.bat clean test` 명령을 실행했을 때 모든 테스트가 아래와 같이 통과하는지 확인한다. - -``` -BUILD SUCCESSFUL in 0s -``` - ---- - ## 🚀 기능 요구사항 반환되는 동전이 최소한이 되는 자판기를 구현한다. @@ -42,6 +15,43 @@ BUILD SUCCESSFUL in 0s - 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 해당 부분부터 다시 입력을 받는다. - 아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다. +### 상품 + +- 이름, 가격, 수량 존재 + +1. '가격'을 리턴 할 수 있어야 한다. +2. 수량을 리턴 가능하다 + +### 상품 더미 + +- 상품 수량 존재 + +1. '상품 최소 가격'을 도출 가능하다 +2. 상품의 총 수량을 도출 가능하다 + +### 자판기 + +- 코인 더미 존재 +- 상품 더미 존재 +- 사용자 입력 금액 존재 + +1. 입력 금액이 상품 더미의 '상품 최소 가격' 보다 적으면 종료한다. +2. 상품의 수량이 모두 0인 경우 종료한다. + +### 코인 더미 +1. 잔돈 계산은 큰 동전부터 진행한다. 500->100-> ... +2. 사용자가 금액을 입력하면 초기화된다. ex) 660 -> 500 + 100 + 50 + 10 + +### exs + +- 10으로 나누어떨어지지 않는 금액이 들어오면 예외이다 +- 입력 미스인 경우 입력을 다시 받는다 + + + + + + ### ✍🏻 입출력 요구사항 #### ⌨️ 입력 @@ -108,46 +118,6 @@ BUILD SUCCESSFUL in 0s 50원 - 1개 ``` ---- - -## 🎱 프로그래밍 요구사항 - -- 프로그램을 실행하는 시작점은 `Application`의 `main()`이다. -- JDK 8 버전에서 실행 가능해야 한다. **JDK 8에서 정상 동작하지 않을 경우 0점 처리**한다. -- 자바 코드 컨벤션을 지키면서 프로그래밍한다. - - https://naver.github.io/hackday-conventions-java -- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. - - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. - - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. -- 3항 연산자를 쓰지 않는다. -- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. - - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. -- else 예약어를 쓰지 않는다. - - 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. - - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. -- 프로그래밍 요구사항에서 별도로 변경 불가 안내가 없는 경우 파일 수정과 패키지 이동을 자유롭게 할 수 있다. - -### 프로그래밍 요구사항 - Coin - -- Coin 클래스를 활용해 구현해야 한다. -- 필드(인스턴스 변수)인 `amount`의 접근 제어자 private을 변경할 수 없다. - -```java -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; - } - - // 추가 기능 구현 -} -``` ### 프로그래밍 요구사항 - Randoms, Console @@ -155,13 +125,3 @@ public enum Coin { - Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInList()`를 활용한다. - 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다. - 프로그램 구현을 완료했을 때 `src/test/java` 디렉터리의 `ApplicationTest`에 있는 모든 테스트 케이스가 성공해야 한다. **테스트가 실패할 경우 0점 처리한다.** - ---- - -## 📈 과제 진행 요구사항 - -- 미션은 [java-vendingmachine-precourse](https://github.com/woowacourse/java-vendingmachine-precourse) 저장소를 Fork/Clone해 시작한다. -- **기능을 구현하기 전에 java-vendingmachine-precourse/docs/README.md 파일에 구현할 기능 목록을 정리**해 추가한다. -- **Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위**로 추가한다. - - [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남긴다. -- 과제 진행 및 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고한다. From d6d136aff4e1205ce376f857c3e65bfd9c14627a Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:43:21 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상품 도메인은 이름,가격,수량을 가진다 - 상품 도메인은 필드를 전부 리턴 가능하다 - 상품 도메인은 수량을 감소 가능하다 --- README.md | 6 +- .../domain/DomainErrorMessage.java | 19 +++++++ .../java/vendingmachine/domain/Product.java | 41 ++++++++++++++ .../vendingmachine/domain/ProductTest.java | 55 +++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 src/main/java/vendingmachine/domain/DomainErrorMessage.java create mode 100644 src/main/java/vendingmachine/domain/Product.java create mode 100644 src/test/java/vendingmachine/domain/ProductTest.java diff --git a/README.md b/README.md index 9a5634ab9..c5f9295cc 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ ### 상품 -- 이름, 가격, 수량 존재 +- 상품 도메인은 이름,가격,수량을 가진다 -1. '가격'을 리턴 할 수 있어야 한다. -2. 수량을 리턴 가능하다 +1. 상품 도메인은 필드를 전부 리턴 가능하다 +2. 상품 도메인은 수량을 감소 가능하다 ### 상품 더미 diff --git a/src/main/java/vendingmachine/domain/DomainErrorMessage.java b/src/main/java/vendingmachine/domain/DomainErrorMessage.java new file mode 100644 index 000000000..8696999a6 --- /dev/null +++ b/src/main/java/vendingmachine/domain/DomainErrorMessage.java @@ -0,0 +1,19 @@ +package vendingmachine.domain; + +public enum DomainErrorMessage { + INVALID_MONEY("가격 형식이 부적절합니다."), + INVALID_BUY_QUANTITY("구매하려는 수량이 재고 수량보다 많습니다."), + + ; + + private static final String ERROR = "[ERROR]"; + private final String message; + + DomainErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return ERROR + message; + } +} diff --git a/src/main/java/vendingmachine/domain/Product.java b/src/main/java/vendingmachine/domain/Product.java new file mode 100644 index 000000000..8b462b6c5 --- /dev/null +++ b/src/main/java/vendingmachine/domain/Product.java @@ -0,0 +1,41 @@ +package vendingmachine.domain; + +public class Product { + private static final int MINIMUM_MONEY_VALUE = 100; + private static final int MINIMUM_MONEY_THRESHOLD = 10; + private final String name; + private final int price; + private int quantity; + + public Product(String name, int price, int quantity) { + this.name = name; + validateMoney(price); + this.price = price; + this.quantity = quantity; + } + + private void validateMoney(int money){ + if (money % MINIMUM_MONEY_THRESHOLD != 0 || money < MINIMUM_MONEY_VALUE){ + throw new IllegalArgumentException(DomainErrorMessage.INVALID_MONEY.getMessage()); + } + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public int getQuantity() { + return quantity; + } + + public void decreaseQuantity(int buyQuantity) { + if (buyQuantity > this.quantity){ + throw new IllegalArgumentException(DomainErrorMessage.INVALID_BUY_QUANTITY.getMessage()); + } + this.quantity -= buyQuantity; + } +} diff --git a/src/test/java/vendingmachine/domain/ProductTest.java b/src/test/java/vendingmachine/domain/ProductTest.java new file mode 100644 index 000000000..f1c4fe73f --- /dev/null +++ b/src/test/java/vendingmachine/domain/ProductTest.java @@ -0,0 +1,55 @@ +package vendingmachine.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +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; + +class ProductTest { + + @DisplayName("초기화에 성공한다.") + @Test + void constructTest() { + String name = "name"; + int price = 1000; + int quantity = 1; + Product product = new Product(name, price, quantity); + SoftAssertions.assertSoftly( + softly -> { + softly.assertThat(product.getName()).isEqualTo(name); + softly.assertThat(product.getPrice()).isEqualTo(price); + softly.assertThat(product.getQuantity()).isEqualTo(quantity); + } + ); + } + + @DisplayName("부적절한 가격이 입력되면 예외이다") + @ParameterizedTest + @ValueSource(ints = {-1, 0, 99, 101}) + void invalid_money(int invalidMoney) { + assertThatThrownBy(() -> new Product("name", invalidMoney, 10)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DomainErrorMessage.INVALID_MONEY.getMessage()); + } + + @DisplayName("수량을 정상적으로 감소 시킬 수 있다.") + @Test + void decreaseQuantity() { + Product product = new Product("name", 100, 10); + product.decreaseQuantity(10); + Assertions.assertThat(product.getQuantity()).isZero(); + } + + @DisplayName("재고 수량보다 더 많은 량을 감소시키면 예외이다.") + @Test + void decreaseQuantity_invalidBuyQuantity() { + Product product = new Product("name", 100, 10); + assertThatThrownBy(() -> product.decreaseQuantity(11)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DomainErrorMessage.INVALID_BUY_QUANTITY.getMessage()); + } +} From 9e7c6354c6f9fc74add6a6df0531bb301a4b292d Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:57:46 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EB=8D=94?= =?UTF-8?q?=EB=AF=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상품 더미는 상품 목록을 가진다 - 상품 최소 가격을 도출한다 - 상품 총 수량을 도출한다 - 상품 더미 초기화 시 빈 리스트나 null 이 들어오면 예외가 발생한다. --- README.md | 8 ++- .../domain/DomainErrorMessage.java | 1 + .../java/vendingmachine/domain/Products.java | 30 +++++++++ .../vendingmachine/domain/ProductTest.java | 1 - .../vendingmachine/domain/ProductsTest.java | 67 +++++++++++++++++++ 5 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/main/java/vendingmachine/domain/Products.java create mode 100644 src/test/java/vendingmachine/domain/ProductsTest.java diff --git a/README.md b/README.md index c5f9295cc..725bd5168 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,12 @@ ### 상품 더미 -- 상품 수량 존재 +- 상품 더미는 상품 목록을 가진다 -1. '상품 최소 가격'을 도출 가능하다 -2. 상품의 총 수량을 도출 가능하다 +1. 상품 최소 가격을 도출한다 +2. 상품 총 수량을 도출한다 + +- 상품 더미 초기화 시 빈 리스트나 null 이 들어오면 예외가 발생한다. ### 자판기 diff --git a/src/main/java/vendingmachine/domain/DomainErrorMessage.java b/src/main/java/vendingmachine/domain/DomainErrorMessage.java index 8696999a6..237818cf4 100644 --- a/src/main/java/vendingmachine/domain/DomainErrorMessage.java +++ b/src/main/java/vendingmachine/domain/DomainErrorMessage.java @@ -3,6 +3,7 @@ public enum DomainErrorMessage { INVALID_MONEY("가격 형식이 부적절합니다."), INVALID_BUY_QUANTITY("구매하려는 수량이 재고 수량보다 많습니다."), + EMPTY_STOCK("상품 목록이 없습니다."), ; diff --git a/src/main/java/vendingmachine/domain/Products.java b/src/main/java/vendingmachine/domain/Products.java new file mode 100644 index 000000000..38fd5a370 --- /dev/null +++ b/src/main/java/vendingmachine/domain/Products.java @@ -0,0 +1,30 @@ +package vendingmachine.domain; + +import java.util.List; +import java.util.Objects; + +public class Products { + private final List stocks; + + public Products(List stocks) { + validateEmpty(stocks); + this.stocks = stocks; + } + + private void validateEmpty(List stocks){ + if (Objects.isNull(stocks) || stocks.isEmpty()){ + throw new IllegalArgumentException(DomainErrorMessage.EMPTY_STOCK.getMessage()); + } + } + + public int getMinimalPrice() { + return stocks.stream() + .map(Product::getPrice) + .min(Integer::compareTo) + .orElse(0); + } + + public int getQuantitySum() { + return stocks.stream().mapToInt(Product::getQuantity).sum(); + } +} diff --git a/src/test/java/vendingmachine/domain/ProductTest.java b/src/test/java/vendingmachine/domain/ProductTest.java index f1c4fe73f..2c711af33 100644 --- a/src/test/java/vendingmachine/domain/ProductTest.java +++ b/src/test/java/vendingmachine/domain/ProductTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.params.provider.ValueSource; class ProductTest { - @DisplayName("초기화에 성공한다.") @Test void constructTest() { diff --git a/src/test/java/vendingmachine/domain/ProductsTest.java b/src/test/java/vendingmachine/domain/ProductsTest.java new file mode 100644 index 000000000..71f263c41 --- /dev/null +++ b/src/test/java/vendingmachine/domain/ProductsTest.java @@ -0,0 +1,67 @@ +package vendingmachine.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.assertj.core.api.Assertions; +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; + +class ProductsTest { + private Products products; + + @BeforeEach + void setUp() { + Product one = new Product("name", 100, 1); + Product two = new Product("name", 200, 2); + Product three = new Product("name", 300, 3); + ArrayList objects = new ArrayList<>(); + objects.add(one); + objects.add(two); + objects.add(three); + products = new Products(Collections.unmodifiableList(objects)); + } + + @DisplayName("초기화에 성공한다.") + @Test + void constructTest() { + Assertions.assertThat(products).isNotNull(); + } + + @DisplayName("상품 목록이 null 이면 예외이다") + @NullSource + @ParameterizedTest + void nullListProvided(List products) { + assertThatThrownBy(() -> new Products(products)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DomainErrorMessage.EMPTY_STOCK.getMessage()); + } + + @DisplayName("상품 목록이 제공되지 않으면 예외이다") + @Test + void invalid_quantity() { + ArrayList empty = new ArrayList<>(); + assertThatThrownBy(() -> new Products(empty)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DomainErrorMessage.EMPTY_STOCK.getMessage()); + } + + @DisplayName("'상품 최소 가격'을 도출 가능하다") + @Test + void getMinimalPrice() { + int minimalPrice = products.getMinimalPrice(); + Assertions.assertThat(minimalPrice).isEqualTo(100); + } + + @DisplayName("상품의 총 수량을 도출 가능하다") + @Test + void getQuantitySum(){ + int quantitySum = products.getQuantitySum(); + Assertions.assertThat(quantitySum).isEqualTo(6); + } +} From 399209484371e4048b67060aaa06db3a17c078f0 Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:42:07 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat=20:=20=EC=BD=94=EC=9D=B8=20=EB=8D=94?= =?UTF-8?q?=EB=AF=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코인더미는 코인 리스트를 가진다. - 금액으로부터 코인 리스트를 생성 할 수 있다 - 입력된 잔돈이 현재 코인 보유량보다 많으면 보유량 전부를 리턴한다 - 입력된 잔돈이 현재 코인 보유량보다 적으면 필요량을 계산하여 리턴한다 - 부적절한 금액이 주어지면 예외가 발생한다 --- README.md | 1 + src/main/java/vendingmachine/Coin.java | 38 ++++++++++++++- .../java/vendingmachine/domain/Coins.java | 38 +++++++++++++++ .../domain/DomainErrorMessage.java | 1 + src/test/java/vendingmachine/CoinTest.java | 32 +++++++++++++ .../java/vendingmachine/domain/CoinsTest.java | 46 +++++++++++++++++++ 6 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/main/java/vendingmachine/domain/Coins.java create mode 100644 src/test/java/vendingmachine/CoinTest.java create mode 100644 src/test/java/vendingmachine/domain/CoinsTest.java diff --git a/README.md b/README.md index 725bd5168..ff391aa8d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ ### 코인 더미 1. 잔돈 계산은 큰 동전부터 진행한다. 500->100-> ... 2. 사용자가 금액을 입력하면 초기화된다. ex) 660 -> 500 + 100 + 50 + 10 +3. 사용되면 리스트에서 제거 할 필요가 없다. 그냥 되는만큼 돌려준다. ### exs diff --git a/src/main/java/vendingmachine/Coin.java b/src/main/java/vendingmachine/Coin.java index c76293fbc..e7f4265f5 100644 --- a/src/main/java/vendingmachine/Coin.java +++ b/src/main/java/vendingmachine/Coin.java @@ -1,16 +1,52 @@ package vendingmachine; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import vendingmachine.domain.DomainErrorMessage; + public enum Coin { COIN_500(500), COIN_100(100), COIN_50(50), COIN_10(10); + private static final int MINIMUM_MONEY_VALUE = 100; + private static final int MINIMUM_MONEY_THRESHOLD = 10; // TODO 이 부분 프로덕트와 중복이다. 제거한다. private final int amount; Coin(final int amount) { this.amount = amount; } - // 추가 기능 구현 + public static List getCoinsFrom(int money){ + validateMoney(money); + return generateCoins(money); + } + + private static List generateCoins(int money) { + List coins = new ArrayList<>(); + for (Coin coin : Coin.values()) { + int count = money / coin.amount; + money %= coin.amount; + addCoins(coin, count, coins); + } + return Collections.unmodifiableList(coins); + } + + private static void addCoins(Coin coin, int count, List coins) { + for (int i = 0; i < count; i++) { + coins.add(coin); + } + } + + private static void validateMoney(int money){ + if (money % MINIMUM_MONEY_THRESHOLD != 0 || money < MINIMUM_MONEY_VALUE){ + throw new IllegalArgumentException(DomainErrorMessage.INVALID_MONEY.getMessage()); + } + } + + public int getAmount() { + return amount; + } } diff --git a/src/main/java/vendingmachine/domain/Coins.java b/src/main/java/vendingmachine/domain/Coins.java new file mode 100644 index 000000000..e7a188062 --- /dev/null +++ b/src/main/java/vendingmachine/domain/Coins.java @@ -0,0 +1,38 @@ +package vendingmachine.domain; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import vendingmachine.Coin; + +public class Coins { + private final List coinsForChanges; + + private Coins(List coinsForChanges) { + validate(coinsForChanges); + this.coinsForChanges = coinsForChanges; + } + + private void validate(List coins){ + if (Objects.isNull(coins) || coins.isEmpty()) { + throw new IllegalArgumentException(DomainErrorMessage.INVALID_CHANGES.getMessage()); + } + } + + public static Coins from(int money){ + List coinsFromMoney = Coin.getCoinsFrom(money); + return new Coins(coinsFromMoney); + } + + private int getCoinsSum(){ + return coinsForChanges.stream().mapToInt(Coin::getAmount).sum(); + } + + public List getChanges(int changes){ + if (getCoinsSum() <= changes){ + return Collections.unmodifiableList(coinsForChanges); + } + return Coin.getCoinsFrom(changes); + // 만약 어플리케이션이 계속 되어야 한다면, 아래 리스트가 포함하는 요소를 Coins 클래스의 리스트에서 제거한다. + } +} diff --git a/src/main/java/vendingmachine/domain/DomainErrorMessage.java b/src/main/java/vendingmachine/domain/DomainErrorMessage.java index 237818cf4..1ef100f01 100644 --- a/src/main/java/vendingmachine/domain/DomainErrorMessage.java +++ b/src/main/java/vendingmachine/domain/DomainErrorMessage.java @@ -4,6 +4,7 @@ public enum DomainErrorMessage { INVALID_MONEY("가격 형식이 부적절합니다."), INVALID_BUY_QUANTITY("구매하려는 수량이 재고 수량보다 많습니다."), EMPTY_STOCK("상품 목록이 없습니다."), + INVALID_CHANGES("잔돈 입력 금액이 부적절 합니다."), ; diff --git a/src/test/java/vendingmachine/CoinTest.java b/src/test/java/vendingmachine/CoinTest.java new file mode 100644 index 000000000..9af901481 --- /dev/null +++ b/src/test/java/vendingmachine/CoinTest.java @@ -0,0 +1,32 @@ +package vendingmachine; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.assertj.core.api.Assertions; +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 vendingmachine.domain.DomainErrorMessage; + +class CoinTest { + @DisplayName("금액으로부터 코인 더미를 생성 할 수 있다") + @Test + void getCoinsFrom() { + List coinsFrom = Coin.getCoinsFrom(660); + Assertions.assertThat(coinsFrom).containsExactly( + Coin.COIN_500, Coin.COIN_100, Coin.COIN_50, Coin.COIN_10 + ); + } + + @DisplayName("부적절한 금액이 주어지면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(ints = {0, 99, 101}) + void getCoinsFrom_invalidMoney(int invalidMoney) { + assertThatThrownBy(() -> Coin.getCoinsFrom(invalidMoney)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(DomainErrorMessage.INVALID_MONEY.getMessage()); + + } +} diff --git a/src/test/java/vendingmachine/domain/CoinsTest.java b/src/test/java/vendingmachine/domain/CoinsTest.java new file mode 100644 index 000000000..8a729daf6 --- /dev/null +++ b/src/test/java/vendingmachine/domain/CoinsTest.java @@ -0,0 +1,46 @@ +package vendingmachine.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vendingmachine.Coin; + +class CoinsTest { + @DisplayName("초기화에 성공한다") + @Test + void constructTest() { + Coins coins = Coins.from(500); + Assertions.assertThat(coins).isNotNull(); + } + + @DisplayName("부적절한 금액이 주어지면 예외가 발생한다.") + @Test + void constructTest_emptyList() { + assertThatThrownBy(() -> Coins.from(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DomainErrorMessage.INVALID_MONEY.getMessage()); + } + + @DisplayName("잔돈이 현재 코인 보유량보다 많으면 보유량 전부를 리턴한다.") + @Test + void getChanges_changeIsBiggerThanCoins() { + Coins coins = Coins.from(660); + List changes = coins.getChanges(1000); + Assertions.assertThat(changes).containsExactly( + Coin.COIN_500, Coin.COIN_100, Coin.COIN_50, Coin.COIN_10 + ); + } + + @DisplayName("잔돈이 현재 코인 보유량보다 적으면 필요량을 계산하여 리턴한다.") + @Test + void getChanges_changeIsSmallerThanCoins() { + Coins coins = Coins.from(1000); + List changes = coins.getChanges(660); + Assertions.assertThat(changes).containsExactly( + Coin.COIN_500, Coin.COIN_100, Coin.COIN_50, Coin.COIN_10 + ); + } +} From fea76b336163645d0dc1bdda5898e6bb3d219421 Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 23:23:02 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat(Products)=20:=20=EC=83=81=ED=92=88?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9D=80=20=EC=83=81=ED=92=88=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EC=83=81=ED=92=88?= =?UTF-8?q?=EC=9D=84=20=EB=B6=88=EB=9F=AC=EC=98=A8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상품은 상품 이름으로 가격을 리턴 가능하다 - 상품 이름으로 재고를 차감 가능하다 - 중복된 상품 이름이 있으면 예외이다 --- README.md | 8 ++-- .../domain/DomainErrorMessage.java | 6 ++- .../java/vendingmachine/domain/Products.java | 38 +++++++++++++++++-- .../vendingmachine/domain/ProductsTest.java | 24 ++++++++++-- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ff391aa8d..4f07b98d6 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ - 상품 더미는 상품 목록을 가진다 -1. 상품 최소 가격을 도출한다 -2. 상품 총 수량을 도출한다 - -- 상품 더미 초기화 시 빈 리스트나 null 이 들어오면 예외가 발생한다. +1. 상품 더미 초기화 시 빈 리스트나 null 이 들어오면 예외가 발생한다. +2. 상품은 상품 이름으로 가격을 리턴 가능하다 +3. 상품 이름으로 재고를 차감 가능하다 +4. 중복된 상품 이름이 있으면 예외이다 ### 자판기 diff --git a/src/main/java/vendingmachine/domain/DomainErrorMessage.java b/src/main/java/vendingmachine/domain/DomainErrorMessage.java index 1ef100f01..3451feb60 100644 --- a/src/main/java/vendingmachine/domain/DomainErrorMessage.java +++ b/src/main/java/vendingmachine/domain/DomainErrorMessage.java @@ -4,9 +4,11 @@ public enum DomainErrorMessage { INVALID_MONEY("가격 형식이 부적절합니다."), INVALID_BUY_QUANTITY("구매하려는 수량이 재고 수량보다 많습니다."), EMPTY_STOCK("상품 목록이 없습니다."), - INVALID_CHANGES("잔돈 입력 금액이 부적절 합니다."), + INVALID_CHANGES("잔돈 입력 금액이 부적절합니다."), + INVALID_PRODUCT_NAME("상품 이름이 부적절합니다."), + NOT_ENOUGH_MONEY("상품 구매 금액이 부족합니다."), - ; + DUPLICATED_NAMES("상품 이름이 중복되었습니다."); private static final String ERROR = "[ERROR]"; private final String message; diff --git a/src/main/java/vendingmachine/domain/Products.java b/src/main/java/vendingmachine/domain/Products.java index 38fd5a370..d49812770 100644 --- a/src/main/java/vendingmachine/domain/Products.java +++ b/src/main/java/vendingmachine/domain/Products.java @@ -2,29 +2,59 @@ import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; public class Products { private final List stocks; public Products(List stocks) { - validateEmpty(stocks); + validate(stocks); this.stocks = stocks; } + private void validate(List stocks){ + validateEmpty(stocks); + validateDuplicatedName(stocks); + } + private void validateEmpty(List stocks){ if (Objects.isNull(stocks) || stocks.isEmpty()){ throw new IllegalArgumentException(DomainErrorMessage.EMPTY_STOCK.getMessage()); } } + private void validateDuplicatedName(List stocks){ + Set nameSet = stocks.stream().map(Product::getName).collect(Collectors.toSet()); + if (stocks.size() != nameSet.size()){ + throw new IllegalArgumentException(DomainErrorMessage.DUPLICATED_NAMES.getMessage()); + } + } + public int getMinimalPrice() { return stocks.stream() - .map(Product::getPrice) - .min(Integer::compareTo) - .orElse(0); + .mapToInt(Product::getPrice) + .min() + .orElseThrow(() -> new IllegalArgumentException(DomainErrorMessage.EMPTY_STOCK.getMessage())); } public int getQuantitySum() { return stocks.stream().mapToInt(Product::getQuantity).sum(); } + + public int getPriceFrom(String productName){ + return getProductByName(productName).getPrice(); + } + + public void decreaseByName(String productName){ + Product productByName = getProductByName(productName); + productByName.decreaseQuantity(1); + } + + private Product getProductByName(String productName) { + return stocks.stream() + .filter(each -> each.getName().equals(productName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(DomainErrorMessage.INVALID_PRODUCT_NAME.getMessage())); + } } diff --git a/src/test/java/vendingmachine/domain/ProductsTest.java b/src/test/java/vendingmachine/domain/ProductsTest.java index 71f263c41..98a599f87 100644 --- a/src/test/java/vendingmachine/domain/ProductsTest.java +++ b/src/test/java/vendingmachine/domain/ProductsTest.java @@ -17,9 +17,9 @@ class ProductsTest { @BeforeEach void setUp() { - Product one = new Product("name", 100, 1); - Product two = new Product("name", 200, 2); - Product three = new Product("name", 300, 3); + Product one = new Product("name1", 100, 1); + Product two = new Product("name2", 200, 2); + Product three = new Product("name3", 300, 3); ArrayList objects = new ArrayList<>(); objects.add(one); objects.add(two); @@ -51,6 +51,17 @@ void invalid_quantity() { .hasMessage(DomainErrorMessage.EMPTY_STOCK.getMessage()); } + @DisplayName("중복된 상품 이름이 입력되면 예외이다.") + @Test + void duplicatedNames() { + ArrayList empty = new ArrayList<>(); + empty.add(new Product("name1", 100, 1)); + empty.add(new Product("name1", 200, 2)); + assertThatThrownBy(() -> new Products(empty)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DomainErrorMessage.DUPLICATED_NAMES.getMessage()); + } + @DisplayName("'상품 최소 가격'을 도출 가능하다") @Test void getMinimalPrice() { @@ -64,4 +75,11 @@ void getQuantitySum(){ int quantitySum = products.getQuantitySum(); Assertions.assertThat(quantitySum).isEqualTo(6); } + + @DisplayName("상품의 이름으로 가격을 불러 올 수 있다.") + @Test + void getPriceFrom() { + int amount = products.getPriceFrom("name1"); + Assertions.assertThat(amount).isEqualTo(100); + } } From 94b40e02cd920de57bf11a8cfd695c2336c95185 Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 23:24:54 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=9A=A9=EB=90=9C=20?= =?UTF-8?q?=EC=BD=94=EC=9D=B8=EC=9D=80=20=EC=BD=94=EC=9D=B8=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0=EB=90=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코인을 가변 컬렉션으로 변경 --- src/main/java/vendingmachine/Coin.java | 3 +-- src/main/java/vendingmachine/domain/Coins.java | 8 +++++--- src/test/java/vendingmachine/domain/CoinsTest.java | 11 +++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/vendingmachine/Coin.java b/src/main/java/vendingmachine/Coin.java index e7f4265f5..2fc8df018 100644 --- a/src/main/java/vendingmachine/Coin.java +++ b/src/main/java/vendingmachine/Coin.java @@ -1,7 +1,6 @@ package vendingmachine; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import vendingmachine.domain.DomainErrorMessage; @@ -31,7 +30,7 @@ private static List generateCoins(int money) { money %= coin.amount; addCoins(coin, count, coins); } - return Collections.unmodifiableList(coins); + return coins; } private static void addCoins(Coin coin, int count, List coins) { diff --git a/src/main/java/vendingmachine/domain/Coins.java b/src/main/java/vendingmachine/domain/Coins.java index e7a188062..7e51b0438 100644 --- a/src/main/java/vendingmachine/domain/Coins.java +++ b/src/main/java/vendingmachine/domain/Coins.java @@ -1,6 +1,5 @@ package vendingmachine.domain; -import java.util.Collections; import java.util.List; import java.util.Objects; import vendingmachine.Coin; @@ -30,9 +29,12 @@ private int getCoinsSum(){ public List getChanges(int changes){ if (getCoinsSum() <= changes){ - return Collections.unmodifiableList(coinsForChanges); + return coinsForChanges; } return Coin.getCoinsFrom(changes); - // 만약 어플리케이션이 계속 되어야 한다면, 아래 리스트가 포함하는 요소를 Coins 클래스의 리스트에서 제거한다. + } + + public void removeAll(List changes) { + coinsForChanges.removeAll(changes); } } diff --git a/src/test/java/vendingmachine/domain/CoinsTest.java b/src/test/java/vendingmachine/domain/CoinsTest.java index 8a729daf6..a9a50ada9 100644 --- a/src/test/java/vendingmachine/domain/CoinsTest.java +++ b/src/test/java/vendingmachine/domain/CoinsTest.java @@ -43,4 +43,15 @@ void getChanges_changeIsSmallerThanCoins() { Coin.COIN_500, Coin.COIN_100, Coin.COIN_50, Coin.COIN_10 ); } + + @DisplayName("사용된 코인은 리스트에서 제거된다.") + @Test + void removeAll() { + Coins coins = Coins.from(1320); + List changes = coins.getChanges(660); + coins.removeAll(changes); + Assertions.assertThat(changes).containsExactly( + Coin.COIN_500, Coin.COIN_100, Coin.COIN_50, Coin.COIN_10 + ); + } } From bd5fec11128077445de42742604bcf597e2af288 Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 23:28:16 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat(ProductParser)=20:=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=EC=9D=98=20=EC=9E=85=EB=A0=A5=EC=9D=84=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=ED=95=B4=EC=84=9C=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=ED=99=98?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자의 입력을 파싱해서 상품 목록으로 변환한다 - 정해진 포맷에 맞지 않으면 예외가 발생한다 - 상품 별 구분자는 ';'여야 한다 - 상품 내 이름,가격 등의 구분자는 ',' 이다 - 상품은 '이름, 가격, 수량' 세 가지 항목 모두를 포함해야 한다. --- README.md | 11 ++++ .../java/vendingmachine/domain/Product.java | 19 +++++++ .../PresentationErrorMessage.java | 18 +++++++ .../presentation/ProductParser.java | 53 +++++++++++++++++++ .../presentation/ProductParserTest.java | 33 ++++++++++++ 5 files changed, 134 insertions(+) create mode 100644 src/main/java/vendingmachine/presentation/PresentationErrorMessage.java create mode 100644 src/main/java/vendingmachine/presentation/ProductParser.java create mode 100644 src/test/java/vendingmachine/presentation/ProductParserTest.java diff --git a/README.md b/README.md index 4f07b98d6..4caba4467 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,17 @@ - 10으로 나누어떨어지지 않는 금액이 들어오면 예외이다 - 입력 미스인 경우 입력을 다시 받는다 +--- + +# 입출력 + +### 상품 파서 + +- 사용자의 입력을 파싱해서 상품 목록으로 변환한다 +- 정해진 포맷에 맞지 않으면 예외가 발생한다 + - 상품 별 구분자는 ';'여야 한다 + - 상품 내 이름,가격 등의 구분자는 ',' 이다 + - 상품은 '이름, 가격, 수량' 세 가지 항목 모두를 포함해야 한다. diff --git a/src/main/java/vendingmachine/domain/Product.java b/src/main/java/vendingmachine/domain/Product.java index 8b462b6c5..f208e7e93 100644 --- a/src/main/java/vendingmachine/domain/Product.java +++ b/src/main/java/vendingmachine/domain/Product.java @@ -1,5 +1,7 @@ package vendingmachine.domain; +import java.util.Objects; + public class Product { private static final int MINIMUM_MONEY_VALUE = 100; private static final int MINIMUM_MONEY_THRESHOLD = 10; @@ -38,4 +40,21 @@ public void decreaseQuantity(int buyQuantity) { } this.quantity -= buyQuantity; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Product)) { + return false; + } + Product product = (Product) o; + return price == product.price && quantity == product.quantity && Objects.equals(name, product.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, price, quantity); + } } diff --git a/src/main/java/vendingmachine/presentation/PresentationErrorMessage.java b/src/main/java/vendingmachine/presentation/PresentationErrorMessage.java new file mode 100644 index 000000000..c2f196c1d --- /dev/null +++ b/src/main/java/vendingmachine/presentation/PresentationErrorMessage.java @@ -0,0 +1,18 @@ +package vendingmachine.presentation; + +public enum PresentationErrorMessage { + INVALID_PRODUCT_INPUT_FORMAT("상품 입력 포맷이 부적절합니다."), + INVALID_NUMBER_FORMAT("숫자만 입력 해 주세요"), + ; + + private static final String ERROR = "[ERROR]"; + private final String message; + + PresentationErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return ERROR + message; + } +} diff --git a/src/main/java/vendingmachine/presentation/ProductParser.java b/src/main/java/vendingmachine/presentation/ProductParser.java new file mode 100644 index 000000000..5f56cbefb --- /dev/null +++ b/src/main/java/vendingmachine/presentation/ProductParser.java @@ -0,0 +1,53 @@ +package vendingmachine.presentation; + +import java.util.ArrayList; +import java.util.List; +import vendingmachine.domain.Product; + +public class ProductParser { + private ProductParser() { + throw new UnsupportedOperationException(); + } + + public static List parseInput(String input){ + ArrayList products = new ArrayList<>(); + String[] split = input.split(";"); + for (String each : split) { + Product product = getProduct(each); + products.add(product); + } + return products; + } + + private static Product getProduct(String each) { + String[] namePriceQuantity = getNamePriceQuantity(each); + String name = namePriceQuantity[0]; + String price = namePriceQuantity[1]; + String quantity = namePriceQuantity[2]; + return new Product(name, formatStringToInteger(price), formatStringToInteger(quantity)); + } + + private static int formatStringToInteger(String value){ + try { + return Integer.parseInt(value); + } catch (NumberFormatException e){ + throw new IllegalArgumentException(PresentationErrorMessage.INVALID_NUMBER_FORMAT.getMessage()); + } + } + + private static String[] getNamePriceQuantity(String each) { + if (!each.startsWith("[") || !each.endsWith("]")) { + throw new IllegalArgumentException(PresentationErrorMessage.INVALID_PRODUCT_INPUT_FORMAT.getMessage()); + } + String productData = each.replace("[", "").replace("]", ""); + String[] namePriceQuantity = productData.split(","); + validateProductFormat(namePriceQuantity); + return namePriceQuantity; + } + + private static void validateProductFormat(String[] namePriceQuantity) { + if (namePriceQuantity.length != 3){ + throw new IllegalArgumentException(PresentationErrorMessage.INVALID_PRODUCT_INPUT_FORMAT.getMessage()); + } + } +} diff --git a/src/test/java/vendingmachine/presentation/ProductParserTest.java b/src/test/java/vendingmachine/presentation/ProductParserTest.java new file mode 100644 index 000000000..0420ab72b --- /dev/null +++ b/src/test/java/vendingmachine/presentation/ProductParserTest.java @@ -0,0 +1,33 @@ +package vendingmachine.presentation; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.assertj.core.api.Assertions; +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 vendingmachine.domain.Product; + +class ProductParserTest { + @DisplayName("사용자의 입력을 파싱해서 상품 목록으로 변환한다.") + @Test + void parseInput() { + String given = "[콜라,1500,20];[사이다,1000,10]"; + List products = ProductParser.parseInput(given); + Product cola = new Product("콜라", 1500, 20); + Product cider = new Product("사이다", 1000, 10); + Assertions.assertThat(products).containsExactly(cola, cider); + } + + @DisplayName("정해진 포맷에 맞지 않으면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"[콜라,1500,20],[사이다,1000,10]", "[콜라,숫자아님,20],[사이다,1000,10]", "[콜라,1000]", "[콜라;1000;10]"}) + void parseInput_invalidDelimiter() { + String given = "[콜라,1500,20],[사이다,1000,10]"; + assertThatThrownBy(() -> ProductParser.parseInput(given)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(PresentationErrorMessage.INVALID_PRODUCT_INPUT_FORMAT.getMessage()); + } +} From 76786d83739f243304d99447bf63a9b0a0f16872 Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 23:28:52 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat(RandomCoinGenerator)=20:=20=EC=9E=90?= =?UTF-8?q?=ED=8C=90=EA=B8=B0=EA=B0=80=20=EA=B0=80=EC=A7=88=20=EC=9E=94?= =?UTF-8?q?=EB=8F=88=EC=9D=80=20=EB=9E=9C=EB=8D=A4=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=83=9D=EC=84=B1=EB=90=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/RandomCoinGenerator.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/vendingmachine/domain/RandomCoinGenerator.java diff --git a/src/main/java/vendingmachine/domain/RandomCoinGenerator.java b/src/main/java/vendingmachine/domain/RandomCoinGenerator.java new file mode 100644 index 000000000..d97e93030 --- /dev/null +++ b/src/main/java/vendingmachine/domain/RandomCoinGenerator.java @@ -0,0 +1,18 @@ +package vendingmachine.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.ArrayList; + +public class RandomCoinGenerator { + private RandomCoinGenerator() { + throw new UnsupportedOperationException(); + } + + public static int getRandomChangeAmount(){ + ArrayList numberRanges = new ArrayList<>(); + numberRanges.add(450); + numberRanges.add(550); + numberRanges.add(650); // 이걸 내가 해야한다고라... + return Randoms.pickNumberInList(numberRanges); + } +} From bf36e119966189291a95050f373889917c457d38 Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Sun, 17 Nov 2024 23:35:08 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat(VendingMachine)=20:=20=EC=9E=90?= =?UTF-8?q?=ED=8C=90=EA=B8=B0=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자판기는 제품을 살 수 있다 - 자판기는 남은 금액으로 제품을 더 살 수 있는지 판별 가능하다. - 자판기는 투입 금액을 리턴 가능하다 - 자판기는 돌려 줄 잔돈을 리턴 가능하다 --- README.md | 7 ++- .../vendingmachine/domain/VendingMachine.java | 42 +++++++++++++ .../domain/VendingMachineTest.java | 60 +++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 src/main/java/vendingmachine/domain/VendingMachine.java create mode 100644 src/test/java/vendingmachine/domain/VendingMachineTest.java diff --git a/README.md b/README.md index 4caba4467..7056483f9 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,11 @@ - 상품 더미 존재 - 사용자 입력 금액 존재 -1. 입력 금액이 상품 더미의 '상품 최소 가격' 보다 적으면 종료한다. -2. 상품의 수량이 모두 0인 경우 종료한다. + +- 자판기는 제품을 살 수 있다 +- 자판기는 남은 금액으로 제품을 더 살 수 있는지 판별 가능하다. +- 자판기는 투입 금액을 리턴 가능하다 +- 자판기는 돌려 줄 잔돈을 리턴 가능하다 ### 코인 더미 1. 잔돈 계산은 큰 동전부터 진행한다. 500->100-> ... diff --git a/src/main/java/vendingmachine/domain/VendingMachine.java b/src/main/java/vendingmachine/domain/VendingMachine.java new file mode 100644 index 000000000..5b7dd0eae --- /dev/null +++ b/src/main/java/vendingmachine/domain/VendingMachine.java @@ -0,0 +1,42 @@ +package vendingmachine.domain; + +import java.util.List; +import vendingmachine.Coin; + +public class VendingMachine { + private final Coins coins; + private final Products products; + private int inputMoney; + + public VendingMachine(Coins coins, Products products, int inputMoney) { + this.coins = coins; + this.products = products; + this.inputMoney = inputMoney; + } + + public void buyProduct(String productName){ + int productPrice = products.getPriceFrom(productName); + if (productPrice > inputMoney){ + throw new IllegalArgumentException(DomainErrorMessage.NOT_ENOUGH_MONEY.getMessage()); + } + inputMoney -= productPrice; + products.decreaseByName(productName); + } + + public boolean canBuy(){ + if (products.getQuantitySum() == 0){ + return false; + } + return products.getMinimalPrice() <= inputMoney; + } + + public int getInputMoney() { + return inputMoney; + } + + public List getChanges(){ + List changes = coins.getChanges(inputMoney); + coins.removeAll(changes); + return changes; + } +} diff --git a/src/test/java/vendingmachine/domain/VendingMachineTest.java b/src/test/java/vendingmachine/domain/VendingMachineTest.java new file mode 100644 index 000000000..9f484fa6f --- /dev/null +++ b/src/test/java/vendingmachine/domain/VendingMachineTest.java @@ -0,0 +1,60 @@ +package vendingmachine.domain; + +import java.util.ArrayList; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vendingmachine.Coin; +import vendingmachine.presentation.ProductParser; + +class VendingMachineTest { + private VendingMachine vendingMachine; + private Coins coins; + private List products; + + @BeforeEach + void setUp() { + coins = Coins.from(1000); + products = ProductParser.parseInput("[콜라,1500,20];[사이다,1000,10]"); + vendingMachine = new VendingMachine(coins, new Products(products), 5000); + } + + @DisplayName("물품을 구매하면 넣은 금액이 차감된다.") + @Test + void buyProduct() { + vendingMachine.buyProduct("콜라"); + Assertions.assertThat(vendingMachine.getInputMoney()).isEqualTo(3500); + } + + @DisplayName("남은 금액이 물품의 최소금액보다 적으면 구매가 불가능하다.") + @Test + void canBuy_notEnoughBudget() { + vendingMachine.buyProduct("콜라"); + vendingMachine.buyProduct("콜라"); + vendingMachine.buyProduct("콜라"); // 남은금액 500원 + Assertions.assertThat(vendingMachine.canBuy()).isFalse(); + } + + @DisplayName("재고가 없으면 구매가 불가능하다.") + @Test + void buyProduct_NotEnoughQuantity() { + ArrayList emptyList = new ArrayList<>(); + Product product = new Product("name", 1000, 0); + emptyList.add(product); + VendingMachine vendingMachine1 = new VendingMachine(coins, new Products(emptyList), 5000); + Assertions.assertThat(vendingMachine1.canBuy()).isFalse(); + } + + @DisplayName("자판기는 잔돈을 계산하여 리턴 가능하다") + @Test + void getChanges() { + vendingMachine = new VendingMachine(coins, new Products(products), 1660); + vendingMachine.buyProduct("사이다"); + List changes = vendingMachine.getChanges(); + Assertions.assertThat(changes).containsExactly( + Coin.COIN_500, Coin.COIN_100, Coin.COIN_50, Coin.COIN_10 + ); + } +} From a9f3c8dff89c919629f34bc57d86a98d16a605db Mon Sep 17 00:00:00 2001 From: calaf <117057567+ca1af@users.noreply.github.com> Date: Mon, 18 Nov 2024 00:12:50 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat=20:=20=EC=96=B4=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=86=B5=EA=B3=BC=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/vendingmachine/Application.java | 9 ++- src/main/java/vendingmachine/Coin.java | 4 ++ .../java/vendingmachine/domain/Coins.java | 4 ++ .../domain/DomainErrorMessage.java | 2 +- .../vendingmachine/domain/VendingMachine.java | 4 ++ .../presentation/CoinsFormatter.java | 30 ++++++++++ .../presentation/InputValidator.java | 13 ++++ .../presentation/InputView.java | 32 ++++++++++ .../presentation/OutputView.java | 19 ++++++ .../PresentationErrorMessage.java | 3 +- .../presentation/RetryHandler.java | 30 ++++++++++ .../VendingMachineController.java | 59 +++++++++++++++++++ 12 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 src/main/java/vendingmachine/presentation/CoinsFormatter.java create mode 100644 src/main/java/vendingmachine/presentation/InputValidator.java create mode 100644 src/main/java/vendingmachine/presentation/InputView.java create mode 100644 src/main/java/vendingmachine/presentation/OutputView.java create mode 100644 src/main/java/vendingmachine/presentation/RetryHandler.java create mode 100644 src/main/java/vendingmachine/presentation/VendingMachineController.java diff --git a/src/main/java/vendingmachine/Application.java b/src/main/java/vendingmachine/Application.java index 9d3be447b..46717a7f4 100644 --- a/src/main/java/vendingmachine/Application.java +++ b/src/main/java/vendingmachine/Application.java @@ -1,7 +1,14 @@ package vendingmachine; +import vendingmachine.presentation.InputView; +import vendingmachine.presentation.OutputView; +import vendingmachine.presentation.VendingMachineController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + VendingMachineController vendingMachineController = new VendingMachineController(inputView, outputView); + vendingMachineController.run(); } } diff --git a/src/main/java/vendingmachine/Coin.java b/src/main/java/vendingmachine/Coin.java index 2fc8df018..64106fc66 100644 --- a/src/main/java/vendingmachine/Coin.java +++ b/src/main/java/vendingmachine/Coin.java @@ -48,4 +48,8 @@ private static void validateMoney(int money){ public int getAmount() { return amount; } + + public String customToString(int quantity) { + return amount + "원 - " + quantity + "개"; + } } diff --git a/src/main/java/vendingmachine/domain/Coins.java b/src/main/java/vendingmachine/domain/Coins.java index 7e51b0438..0c4ed15c3 100644 --- a/src/main/java/vendingmachine/domain/Coins.java +++ b/src/main/java/vendingmachine/domain/Coins.java @@ -37,4 +37,8 @@ public List getChanges(int changes){ public void removeAll(List changes) { coinsForChanges.removeAll(changes); } + + public List getCoinsForChanges() { + return coinsForChanges; + } } diff --git a/src/main/java/vendingmachine/domain/DomainErrorMessage.java b/src/main/java/vendingmachine/domain/DomainErrorMessage.java index 3451feb60..9cf743695 100644 --- a/src/main/java/vendingmachine/domain/DomainErrorMessage.java +++ b/src/main/java/vendingmachine/domain/DomainErrorMessage.java @@ -10,7 +10,7 @@ public enum DomainErrorMessage { DUPLICATED_NAMES("상품 이름이 중복되었습니다."); - private static final String ERROR = "[ERROR]"; + private static final String ERROR = "[ERROR] "; private final String message; DomainErrorMessage(String message) { diff --git a/src/main/java/vendingmachine/domain/VendingMachine.java b/src/main/java/vendingmachine/domain/VendingMachine.java index 5b7dd0eae..694164dbb 100644 --- a/src/main/java/vendingmachine/domain/VendingMachine.java +++ b/src/main/java/vendingmachine/domain/VendingMachine.java @@ -39,4 +39,8 @@ public List getChanges(){ coins.removeAll(changes); return changes; } + + public String getCoinsString(){ + return coins.toString(); + } } diff --git a/src/main/java/vendingmachine/presentation/CoinsFormatter.java b/src/main/java/vendingmachine/presentation/CoinsFormatter.java new file mode 100644 index 000000000..2993f6a74 --- /dev/null +++ b/src/main/java/vendingmachine/presentation/CoinsFormatter.java @@ -0,0 +1,30 @@ +package vendingmachine.presentation; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import vendingmachine.Coin; + +public class CoinsFormatter { + private final Map coinMap; + + public CoinsFormatter(List coins) { + coinMap = new EnumMap<>(Coin.class); + for (Coin coin : Coin.values()) { + coinMap.put(coin, 0); + } + + coins.forEach(coin -> coinMap.put(coin, coinMap.get(coin) + 1)); + } + + public String format() { + return coinMap.entrySet().stream() + .map(CoinsFormatter::formatEntry) + .collect(Collectors.joining(System.lineSeparator())); + } + + private static String formatEntry(Map.Entry entry) { + return entry.getKey().getAmount() + "원 - " + entry.getValue() + "개"; + } +} diff --git a/src/main/java/vendingmachine/presentation/InputValidator.java b/src/main/java/vendingmachine/presentation/InputValidator.java new file mode 100644 index 000000000..fbd29b27e --- /dev/null +++ b/src/main/java/vendingmachine/presentation/InputValidator.java @@ -0,0 +1,13 @@ +package vendingmachine.presentation; + +public class InputValidator { + private InputValidator() { + throw new UnsupportedOperationException(); + } + + public static void validateInteger(int value){ + if (value < 0){ + throw new IllegalArgumentException(PresentationErrorMessage.INVALID_NUMBER.getMessage()); + } + } +} diff --git a/src/main/java/vendingmachine/presentation/InputView.java b/src/main/java/vendingmachine/presentation/InputView.java new file mode 100644 index 000000000..d6a333be2 --- /dev/null +++ b/src/main/java/vendingmachine/presentation/InputView.java @@ -0,0 +1,32 @@ +package vendingmachine.presentation; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + private static final String INPUT_PRODUCT_INFO = "상품명과 가격, 수량을 입력해 주세요."; + private static final String INPUT_MONEY = "투입 금액을 입력해 주세요."; + private static final String COINS_INPUT = "자판기가 보유하고 있는 금액을 입력해 주세요."; + private static final String INPUT_PRODUCT_NAME = "구매할 상품명을 입력해 주세요."; + + public int getChangesInfo(){ + System.out.println(COINS_INPUT); + int coinsValue = Integer.parseInt(Console.readLine()); + InputValidator.validateInteger(coinsValue); + return coinsValue; + } + + public String getProduct(){ + System.out.println(INPUT_PRODUCT_INFO); + return Console.readLine(); + } + + public String getInputMoney(){ + System.out.println(INPUT_MONEY); + return Console.readLine(); + } + + public String getProductName(){ + System.out.println(INPUT_PRODUCT_NAME); + return Console.readLine(); + } +} diff --git a/src/main/java/vendingmachine/presentation/OutputView.java b/src/main/java/vendingmachine/presentation/OutputView.java new file mode 100644 index 000000000..1de3cd103 --- /dev/null +++ b/src/main/java/vendingmachine/presentation/OutputView.java @@ -0,0 +1,19 @@ +package vendingmachine.presentation; + +public class OutputView { + private static final String INPUT_MONEY_REMAIN = "투입 금액: %d원"; + private static final String MACHINE_HOLDING_COINS = "자판기가 보유한 동전"; + + public void printRemainingMoney(int amount){ + System.out.printf((INPUT_MONEY_REMAIN) + "%n", amount); + } + + public void printCoins(String coins){ + System.out.println(coins); + } + + public void printInitialCoins(String coins) { + System.out.println(MACHINE_HOLDING_COINS); + System.out.println(coins); + } +} diff --git a/src/main/java/vendingmachine/presentation/PresentationErrorMessage.java b/src/main/java/vendingmachine/presentation/PresentationErrorMessage.java index c2f196c1d..7cacbe917 100644 --- a/src/main/java/vendingmachine/presentation/PresentationErrorMessage.java +++ b/src/main/java/vendingmachine/presentation/PresentationErrorMessage.java @@ -3,9 +3,10 @@ public enum PresentationErrorMessage { INVALID_PRODUCT_INPUT_FORMAT("상품 입력 포맷이 부적절합니다."), INVALID_NUMBER_FORMAT("숫자만 입력 해 주세요"), + INVALID_NUMBER("부적절한 숫자 범위입니다."), ; - private static final String ERROR = "[ERROR]"; + private static final String ERROR = "[ERROR] "; private final String message; PresentationErrorMessage(String message) { diff --git a/src/main/java/vendingmachine/presentation/RetryHandler.java b/src/main/java/vendingmachine/presentation/RetryHandler.java new file mode 100644 index 000000000..fe89753dd --- /dev/null +++ b/src/main/java/vendingmachine/presentation/RetryHandler.java @@ -0,0 +1,30 @@ +package vendingmachine.presentation; + +import java.util.function.Supplier; + +public class RetryHandler { + private RetryHandler() { + throw new UnsupportedOperationException(); + } + + public static T retry(Supplier consumer){ + while (true){ + try { + return consumer.get(); + } catch (IllegalArgumentException e){ + System.out.println(e.getMessage()); + } + } + } + + public static void retry(Runnable consumer){ + while (true){ + try { + consumer.run(); + return; + } catch (IllegalArgumentException e){ + System.out.println(e.getMessage()); + } + } + } +} diff --git a/src/main/java/vendingmachine/presentation/VendingMachineController.java b/src/main/java/vendingmachine/presentation/VendingMachineController.java new file mode 100644 index 000000000..a8390f1ee --- /dev/null +++ b/src/main/java/vendingmachine/presentation/VendingMachineController.java @@ -0,0 +1,59 @@ +package vendingmachine.presentation; + +import java.util.List; +import vendingmachine.domain.Coins; +import vendingmachine.domain.Product; +import vendingmachine.domain.Products; +import vendingmachine.domain.VendingMachine; + +public class VendingMachineController { + private final InputView inputView; + private final OutputView outputView; + + public VendingMachineController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run(){ + int changesInfo = RetryHandler.retry(inputView::getChangesInfo); + Products products = RetryHandler.retry(this::initProducts); + int inputMoney = RetryHandler.retry(this::getInputMoney); + Coins coins = RetryHandler.retry(() -> getCoins(changesInfo)); + VendingMachine vendingMachine = RetryHandler.retry(() -> new VendingMachine(coins, products, inputMoney)); + RetryHandler.retry(() -> purchaseProducts(vendingMachine)); + RetryHandler.retry(() -> printRemainingCoins(vendingMachine)); + } + + private Coins getCoins(int changesInfo) { + Coins coins = Coins.from(changesInfo); + CoinsFormatter coinsFormatter = new CoinsFormatter(coins.getCoinsForChanges()); + outputView.printInitialCoins(coinsFormatter.format()); + return coins; + } + + private void purchaseProducts(VendingMachine vendingMachine) { + while (vendingMachine.canBuy()){ + outputView.printRemainingMoney(vendingMachine.getInputMoney()); + String productName = inputView.getProductName(); + vendingMachine.buyProduct(productName); + } + } + + private void printRemainingCoins(VendingMachine vendingMachine) { + CoinsFormatter coinsFormatter = new CoinsFormatter(vendingMachine.getChanges()); + String formattedCoins = coinsFormatter.format(); + outputView.printCoins(formattedCoins); + } + + private Products initProducts(){ + String userInput = inputView.getProduct(); + List products = ProductParser.parseInput(userInput); + return new Products(products); + } + + private int getInputMoney(){ + String userInput = inputView.getInputMoney(); + return Integer.parseInt(userInput); + } +}