Skip to content
Open
131 changes: 54 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```

---

## 🚀 기능 요구사항

반환되는 동전이 최소한이 되는 자판기를 구현한다.
Expand All @@ -42,6 +15,60 @@ BUILD SUCCESSFUL in 0s
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 해당 부분부터 다시 입력을 받는다.
- 아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다.

### 상품

- 상품 도메인은 이름,가격,수량을 가진다

1. 상품 도메인은 필드를 전부 리턴 가능하다
2. 상품 도메인은 수량을 감소 가능하다

### 상품 더미

- 상품 더미는 상품 목록을 가진다

1. 상품 더미 초기화 시 빈 리스트나 null 이 들어오면 예외가 발생한다.
2. 상품은 상품 이름으로 가격을 리턴 가능하다
3. 상품 이름으로 재고를 차감 가능하다
4. 중복된 상품 이름이 있으면 예외이다

### 자판기

- 코인 더미 존재
- 상품 더미 존재
- 사용자 입력 금액 존재


- 자판기는 제품을 살 수 있다
- 자판기는 남은 금액으로 제품을 더 살 수 있는지 판별 가능하다.
- 자판기는 투입 금액을 리턴 가능하다
- 자판기는 돌려 줄 잔돈을 리턴 가능하다

### 코인 더미
1. 잔돈 계산은 큰 동전부터 진행한다. 500->100-> ...
2. 사용자가 금액을 입력하면 초기화된다. ex) 660 -> 500 + 100 + 50 + 10
3. 사용되면 리스트에서 제거 할 필요가 없다. 그냥 되는만큼 돌려준다.

### exs

- 10으로 나누어떨어지지 않는 금액이 들어오면 예외이다
- 입력 미스인 경우 입력을 다시 받는다

---

# 입출력

### 상품 파서

- 사용자의 입력을 파싱해서 상품 목록으로 변환한다
- 정해진 포맷에 맞지 않으면 예외가 발생한다
- 상품 별 구분자는 ';'여야 한다
- 상품 내 이름,가격 등의 구분자는 ',' 이다
- 상품은 '이름, 가격, 수량' 세 가지 항목 모두를 포함해야 한다.





### ✍🏻 입출력 요구사항

#### ⌨️ 입력
Expand Down Expand Up @@ -108,60 +135,10 @@ 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

- JDK에서 기본 제공하는 Random, Scanner API 대신 `camp.nextstep.edu.missionutils`에서 제공하는 `Randoms`, `Console` API를 활용해 구현해야 한다.
- 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) 를 참고한다.
9 changes: 8 additions & 1 deletion src/main/java/vendingmachine/Application.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
41 changes: 40 additions & 1 deletion src/main/java/vendingmachine/Coin.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,55 @@
package vendingmachine;

import java.util.ArrayList;
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<Coin> getCoinsFrom(int money){
validateMoney(money);
return generateCoins(money);
}

private static List<Coin> generateCoins(int money) {
List<Coin> coins = new ArrayList<>();
for (Coin coin : Coin.values()) {
int count = money / coin.amount;
money %= coin.amount;
addCoins(coin, count, coins);
}
return coins;
}
Comment on lines +26 to +34

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 방법은 가격이 높은 동전부터 최대한으로 생성하는 방식으로 보입니다. (450원 -> 500원 최대 0개, 100원 최대 4개, 50원 최대 1개...)
요구사항을 확인해보면,
"자판기가 보유하고 있는 금액을 입력하면 무작위로 동전을 생성한다" -> 동전생성은 무작위
"잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려준다." -> 잔돈을 돌려줘야 할때 최소의 갯수로 반환
이기 때문에 코인 생성 방식을 제공받은 Random 라이브러리를 활용해 생성하는 방식으로 전환할 필요성이 있어 보입니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 요구사항이 몬지 정확히 이해를 못했었음...어케해야대냐 이거


private static void addCoins(Coin coin, int count, List<Coin> 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;
}

public String customToString(int quantity) {
return amount + "원 - " + quantity + "개";
}
}
44 changes: 44 additions & 0 deletions src/main/java/vendingmachine/domain/Coins.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package vendingmachine.domain;

import java.util.List;
import java.util.Objects;
import vendingmachine.Coin;

public class Coins {
private final List<Coin> coinsForChanges;

private Coins(List<Coin> coinsForChanges) {
validate(coinsForChanges);
this.coinsForChanges = coinsForChanges;
}

private void validate(List<Coin> coins){
if (Objects.isNull(coins) || coins.isEmpty()) {
throw new IllegalArgumentException(DomainErrorMessage.INVALID_CHANGES.getMessage());
}
}

public static Coins from(int money){
List<Coin> coinsFromMoney = Coin.getCoinsFrom(money);
return new Coins(coinsFromMoney);
}

private int getCoinsSum(){
return coinsForChanges.stream().mapToInt(Coin::getAmount).sum();
}

public List<Coin> getChanges(int changes){
if (getCoinsSum() <= changes){
return coinsForChanges;
}
return Coin.getCoinsFrom(changes);
}

public void removeAll(List<Coin> changes) {
coinsForChanges.removeAll(changes);
}

public List<Coin> getCoinsForChanges() {
return coinsForChanges;
}
}
23 changes: 23 additions & 0 deletions src/main/java/vendingmachine/domain/DomainErrorMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package vendingmachine.domain;

public enum DomainErrorMessage {
INVALID_MONEY("가격 형식이 부적절합니다."),
INVALID_BUY_QUANTITY("구매하려는 수량이 재고 수량보다 많습니다."),
EMPTY_STOCK("상품 목록이 없습니다."),
INVALID_CHANGES("잔돈 입력 금액이 부적절합니다."),
INVALID_PRODUCT_NAME("상품 이름이 부적절합니다."),
NOT_ENOUGH_MONEY("상품 구매 금액이 부족합니다."),

DUPLICATED_NAMES("상품 이름이 중복되었습니다.");

private static final String ERROR = "[ERROR] ";
private final String message;

DomainErrorMessage(String message) {
this.message = message;
}

public String getMessage() {
return ERROR + message;
}
}
60 changes: 60 additions & 0 deletions src/main/java/vendingmachine/domain/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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;
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;
}

@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);
}
}
Loading