ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,59 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_NAME;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_PRICE;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_QUANTITY;
+import static vendingmachine.constant.ErrorMessage.PRODUCT_SOLD_OUT;
+import static vendingmachine.constant.NumberConstant.*;
+
+public class Product {
+    private final String productName;
+    private final int price;
+    private int quantity;
+
+    public Product(String productName, int price, int quantity) {
+        validateProductName(productName);
+        validatePrice(price);
+        validateQuantity(quantity);
+        this.productName = productName;
+        this.price = price;
+        this.quantity = quantity;
+    }
+
+    private void validateProductName(String productName) {
+        if (productName == null || productName.isEmpty()) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_NAME.getMessage());
+        }
+    }
+
+    private void validatePrice(int price) {
+        if (price < MINIMUM_MONEY_PRICE || price % DIVIDE_MONEY_UNIT != 0) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_PRICE.getMessage());
+        }
+    }
+
+    private void validateQuantity(int quantity) {
+        if (quantity < 0) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_QUANTITY.getMessage());
+        }
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public boolean isNameSame(String inputProductName) {
+        return this.productName.equals(inputProductName);
+    }
+
+    public boolean isQuantityZero() {
+        return this.quantity == 0;
+    }
+
+    public void decreaseQuantity() {
+        if (quantity <= 0) {
+            throw new IllegalArgumentException(PRODUCT_SOLD_OUT.getMessage());
+        }
+        this.quantity -= 1;
+    }
+}

리뷰: 수량까지 final로 선언해서 불변 객체로 활용하는 건 어떻게 생각하시나요??
@@ -0,0 +1,43 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.PRODUCT_IS_NOT_EXIST;
+import static vendingmachine.constant.ErrorMessage.NOT_ENOUGH_MONEY;
+
+import java.util.List;
+
+public class Products {
+    private final List<Product> products;
+
+    public Products(List<Product> products) {
+        this.products = products;
+    }
+
+    public int findPriceByProductName(String productName) {
+        return products.stream()
+            .filter(product -> product.isNameSame(productName))
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(PRODUCT_IS_NOT_EXIST.getMessage()))
+            .getPrice();
+    }
+
+    public int findMinimumPrice() {
+        return products.stream()
+            .filter(product -> !product.isQuantityZero())
+            .mapToInt(Product::getPrice)
+            .min()
+            .orElseThrow(() -> new IllegalArgumentException(NOT_ENOUGH_MONEY.getMessage()));
+    }
+
+    public boolean areSoldOut() {
+        return products.stream().allMatch(Product::isQuantityZero);
+    }
+
+    public void decreaseProductQuantity(String productName) {
+        Product product = products.stream()
+            .filter(name -> name.isNameSame(productName))
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(PRODUCT_IS_NOT_EXIST.getMessage()));
+
+        product.decreaseQuantity();
+    }
+}

리뷰: stream을 잘 활용하시는 것 같아요 👍
@@ -0,0 +1,38 @@
+package vendingmachine.model.domain;
+
+public class VendingMachine {
+    private final Coins coins;
+    private Products products;
+
+    public VendingMachine(Coins coins) {
+        this.coins = coins;
+    }
+
+    public Coins getCoins() {
+        return coins;
+    }
+
+    public void addProducts(Products products) {
+        this.products = products;
+    }
+
+    public int findProductPrice(String buyProductName) {
+        return products.findPriceByProductName(buyProductName);
+    }
+
+    public int findMinimumProductPrice() {
+        return products.findMinimumPrice();
+    }
+
+    public boolean areProductsSoldOut() {
+        return products.areSoldOut();
+    }
+
+    public void purchaseProduct(String buyProductName) {
+        products.decreaseProductQuantity(buyProductName);
+    }
+
+    public Coins returnChange(int amount) {
+        return coins.calculateChange(amount);
+    }
+}

리뷰: 일급 컬렉션을 활용하니 확실히 관심사의 분리가 적절하게 이루어지는 것 같아요 👍
@@ -0,0 +1,73 @@
+package vendingmachine.util;
+
+import static vendingmachine.constant.ErrorMessage.MONEY_SHOULD_BE_NUMBER;
+import static vendingmachine.constant.ErrorMessage.MONEY_SHOULD_BE_POSITIVE;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_FORMAT;
+import static vendingmachine.constant.NumberConstant.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import vendingmachine.model.domain.Product;
+import vendingmachine.model.domain.Products;
+
+public class Parser {
+    private static final String SEMICOLON = ";";
+    private static final String LEFT_BRACKET = "[";
+    private static final String RIGHT_BRACKET = "]";
+    private static final String COMMA = ",";
+
+    public static int parseInteger(String input) {
+        try {
+            int money = Integer.parseInt(input);
+            validateNegative(money);
+            return money;
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(MONEY_SHOULD_BE_NUMBER.getMessage());
+        }
+    }
+
+    private static void validateNegative(int money) {
+        if(money < 0) {
+            throw new IllegalArgumentException(MONEY_SHOULD_BE_POSITIVE.getMessage());
+        }
+    }
+
+    public static Products parseProducts(String inputProducts) {
+        List<Product> products = parseProductList(inputProducts);
+        return new Products(products);
+    }
+
+    private static List<Product> parseProductList(String inputProducts) {
+        String[] splitProducts = inputProducts.split(SEMICOLON);
+        List<Product> products = new ArrayList<>();
+
+        for (String splitProduct : splitProducts) {
+            validateProductFormat(splitProduct);
+            Product product = parseProduct(splitProduct);
+            products.add(product);
+        }
+        return products;
+    }
+
+    private static void validateProductFormat(String productInfo) {
+        if (!productInfo.startsWith(LEFT_BRACKET) || !productInfo.endsWith(RIGHT_BRACKET)) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_FORMAT.getMessage());
+        }
+    }
+
+    private static Product parseProduct(String productInfo) {
+        String content = productInfo.substring(1, productInfo.length() - 1);
+        String[] splitProductInfo = content.split(COMMA);
+
+        if (splitProductInfo.length != PRODUCTS_LENGTH) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_FORMAT.getMessage());
+        }
+
+        String name = splitProductInfo[0].trim();
+        int price = Integer.parseInt(splitProductInfo[1].trim());
+        int quantity = Integer.parseInt(splitProductInfo[2].trim());
+
+        return new Product(name, price, quantity);
+    }
+}

리뷰: 정규표현식을 사용하지 않은 이유가 있나요?!
@@ -0,0 +1,44 @@
+package vendingmachine.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import vendingmachine.model.domain.Products;
+import vendingmachine.util.Parser;
+
+public class InputView {
+    public static int vendingMachineHave() {
+        while (true) {
+            try {
+                String money = Console.readLine();
+                return Parser.parseInteger(money);
+            } catch (IllegalArgumentException e) {
+                OutputView.promptErrorMessage(e.getMessage());
+            }
+        }
+    }
+
+    public static Products buyProducts() {
+        while (true) {
+            try {
+                String products = Console.readLine();
+                return Parser.parseProducts(products);
+            } catch (IllegalArgumentException e) {
+                OutputView.promptErrorMessage(e.getMessage());
+            }
+        }
+    }
+
+    public static int inputMoney() {
+        while (true) {
+            try {
+                String money = Console.readLine();
+                return Parser.parseInteger(money);
+            } catch (IllegalArgumentException e) {
+                OutputView.promptErrorMessage(e.getMessage());
+            }
+        }
+    }
+
+    public static String buyProduct() {
+        return Console.readLine();
+    }
+}

리뷰: 재입력을 input단에서 진행해주셨네요!
몇 가지 궁금한 점이 있습니다.

1. 재입력 시 입력 유도 문구는 재출력되지 않나요??
2. 입력형식이 아닌 다른 잘못된 입력 시에는 어떻게 재입력이 진행되나요? (ex. 매진된 상품 구매 요청, 존재하지 않는 상품명 입력 등)
@@ -0,0 +1,36 @@
+package vendingmachine.model.domain;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class ProductTest {
+    @Test
+    @DisplayName("상품 가격이 100원부터 시작하지 않는 경우 예외가 발생한다.")
+    void 상품_가격이_100원부터_시작하지_않는_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("콜라", 90, 1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @DisplayName("상품 가격이 10원으로 나누어 떨어지지지 않는 경우 예외가 발생한다.")
+    void 상품_가격이_10원으로_나누어_떨어지지_않는_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("콜라", 9, 1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @DisplayName("상품명이 비어있는 경 예외가 발생한다.")
+    void 상품명이_비어있는_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("", 1000, 1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @DisplayName("상품 수량이 0 미만인 경우 예외가 발생한다.")
+    void 상품_수량이_0_미만인_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("콜라", 1000, -1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+}

리뷰: TDD 이후로 테스트 코드는 추가로 리팩토링하지 않으신 건가요??
@@ -0,0 +1,7 @@
+package vendingmachine.constant;
+
+public class NumberConstant {
+    public static final int MINIMUM_MONEY_PRICE = 100;
+    public static final int DIVIDE_MONEY_UNIT = 10;
+    public static final int PRODUCTS_LENGTH = 3;
+}

리뷰: 이게 무슨 숫자를 의미하는 건지, 왜 3인지 잘 모르겠어요
@@ -0,0 +1,37 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.*;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public enum Coin {
+    COIN_500(500),
+    COIN_100(100),
+    COIN_50(50),
+    COIN_10(10);
+
+    private final int amount;
+
+    Coin(final int amount) {
+        this.amount = amount;
+    }
+
+    public int getAmount() {
+        return amount;
+    }
+
+    public static List<Integer> getAmounts() {
+        return Arrays.stream(Coin.values())
+            .map(Coin::getAmount)
+            .collect(Collectors.toList());
+    }
+
+    public static Coin from(int amount) {
+        return Arrays.stream(values())
+            .filter(coin -> coin.getAmount() == amount)
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(INVALID_COIN_MONEY.getMessage()));
+    }
+}

리뷰: 오옷 ㅎㅎㅎ 패키지 안에 클래스 5개만 넣기 잘 지키셨군요!!
@@ -0,0 +1,59 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_NAME;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_PRICE;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_QUANTITY;
+import static vendingmachine.constant.ErrorMessage.PRODUCT_SOLD_OUT;
+import static vendingmachine.constant.NumberConstant.*;
+
+public class Product {
+    private final String productName;
+    private final int price;
+    private int quantity;
+
+    public Product(String productName, int price, int quantity) {
+        validateProductName(productName);
+        validatePrice(price);
+        validateQuantity(quantity);
+        this.productName = productName;
+        this.price = price;
+        this.quantity = quantity;
+    }
+
+    private void validateProductName(String productName) {
+        if (productName == null || productName.isEmpty()) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_NAME.getMessage());
+        }
+    }
+
+    private void validatePrice(int price) {
+        if (price < MINIMUM_MONEY_PRICE || price % DIVIDE_MONEY_UNIT != 0) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_PRICE.getMessage());
+        }
+    }
+
+    private void validateQuantity(int quantity) {
+        if (quantity < 0) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_QUANTITY.getMessage());
+        }
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public boolean isNameSame(String inputProductName) {
+        return this.productName.equals(inputProductName);
+    }
+
+    public boolean isQuantityZero() {
+        return this.quantity == 0;
+    }
+
+    public void decreaseQuantity() {
+        if (quantity <= 0) {
+            throw new IllegalArgumentException(PRODUCT_SOLD_OUT.getMessage());
+        }
+        this.quantity -= 1;
+    }
+}

리뷰: 구매 후 재고 수량을 조절해야 할 텐데 불변 객체로 선언해도 될까요?
@@ -0,0 +1,75 @@
+package vendingmachine.controller;
+
+import vendingmachine.model.domain.Coins;
+import vendingmachine.model.domain.Products;
+import vendingmachine.model.domain.VendingMachine;
+import vendingmachine.model.service.VendingMachineService;
+import vendingmachine.view.InputView;
+import vendingmachine.view.OutputView;
+
+public class VendingMachineController {
+    private final VendingMachineService vendingMachineService;
+
+    public VendingMachineController(VendingMachineService vendingMachineService) {
+        this.vendingMachineService = vendingMachineService;
+    }
+
+    public void start() {
+        OutputView.promptInputMachineHave();
+        int money = InputView.vendingMachineHave();
+
+        VendingMachine vendingMachine = vendingMachineService.getVendingMachine(money);
+        OutputView.promptMachineHave(vendingMachine);
+
+        getProducts(vendingMachine);
+        int inputMoney = getInputMoney();
+
+        inputMoney = getInputMoney(vendingMachine, inputMoney);
+        getReturnChange(vendingMachine, inputMoney);
+    }
+
+    private static void getProducts(VendingMachine vendingMachine) {
+        OutputView.promptInputProducts();
+        Products products = InputView.buyProducts();
+        vendingMachine.addProducts(products);
+    }
+
+    private static int getInputMoney() {
+        OutputView.promptInputMoney();
+        return InputView.inputMoney();
+    }
+
+    private static int getInputMoney(VendingMachine vendingMachine, int inputMoney) {
+        while (inputMoney > 0) {
+            int minPrice = vendingMachine.findMinimumProductPrice();
+            if (inputMoney < minPrice) {
+                break;
+            }
+            if (vendingMachine.areProductsSoldOut()) {
+                break;
+            }
+
+            OutputView.promptBuying(inputMoney);
+            String buyProductName = InputView.buyProduct();
+
+            inputMoney = calculateInputMoney(vendingMachine, inputMoney, buyProductName);
+        }
+        return inputMoney;
+    }
+
+    private static void getReturnChange(VendingMachine vendingMachine, int inputMoney) {
+        Coins change = vendingMachine.returnChange(inputMoney);
+        OutputView.promptReturnChange(change);
+    }
+
+    private static int calculateInputMoney(VendingMachine vendingMachine, int inputMoney, String buyProductName) {
+        try {
+            int productPrice = vendingMachine.findProductPrice(buyProductName);
+            inputMoney -= productPrice;
+            vendingMachine.purchaseProduct(buyProductName);
+        } catch (IllegalArgumentException e) {
+            OutputView.promptErrorMessage(e.getMessage());
+        }
+        return inputMoney;
+    }
+}

리뷰: 이 조건들은 while문 조건으로 안 넣고 따로 빼신 이유가 궁금해요
@@ -0,0 +1,37 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.*;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public enum Coin {
+    COIN_500(500),
+    COIN_100(100),
+    COIN_50(50),
+    COIN_10(10);
+
+    private final int amount;
+
+    Coin(final int amount) {
+        this.amount = amount;
+    }
+
+    public int getAmount() {
+        return amount;
+    }
+
+    public static List<Integer> getAmounts() {
+        return Arrays.stream(Coin.values())
+            .map(Coin::getAmount)
+            .collect(Collectors.toList());
+    }
+
+    public static Coin from(int amount) {
+        return Arrays.stream(values())
+            .filter(coin -> coin.getAmount() == amount)
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(INVALID_COIN_MONEY.getMessage()));
+    }
+}

리뷰: 이 부분이 선권님이랑 거의 동일해서 알아봤더니 Enum 관련 관용적 패턴인 거 같던데 맞나요..?!! 안 그래도 enum 클래스 사용법을 잘 몰라서 초반에 시간이 많이 지체됐는데, 이렇게 쓰는 거군용
@@ -0,0 +1,43 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.PRODUCT_IS_NOT_EXIST;
+import static vendingmachine.constant.ErrorMessage.NOT_ENOUGH_MONEY;
+
+import java.util.List;
+
+public class Products {
+    private final List<Product> products;
+
+    public Products(List<Product> products) {
+        this.products = products;
+    }
+
+    public int findPriceByProductName(String productName) {
+        return products.stream()
+            .filter(product -> product.isNameSame(productName))
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(PRODUCT_IS_NOT_EXIST.getMessage()))
+            .getPrice();
+    }
+
+    public int findMinimumPrice() {
+        return products.stream()
+            .filter(product -> !product.isQuantityZero())
+            .mapToInt(Product::getPrice)
+            .min()
+            .orElseThrow(() -> new IllegalArgumentException(NOT_ENOUGH_MONEY.getMessage()));
+    }
+
+    public boolean areSoldOut() {
+        return products.stream().allMatch(Product::isQuantityZero);
+    }
+
+    public void decreaseProductQuantity(String productName) {
+        Product product = products.stream()
+            .filter(name -> name.isNameSame(productName))
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(PRODUCT_IS_NOT_EXIST.getMessage()));
+
+        product.decreaseQuantity();
+    }
+}

리뷰: isQuantityZero 부분은 Product에 두고 순회하는 부분만 products가 가지고 있으니까 좋네요!!
@@ -165,3 +165,47 @@ public enum Coin {
 - **Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위**로 추가한다.
    - [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남긴다.
 - 과제 진행 및 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고한다.
+
+---
+
+## 기능 구현 목록
+- [x] [자판기] 사용자로부터 자판기가 보유하고 있는 금액을 입력받는 기능
+
+
+- [x] [자판기] 자판기가 보유하고 있는 금액만큼 동전을 무작위로 생성하는 기능
+  - [x] 투입 금액으로는 동전을 생성하지 않는다.
+
+
+- [x] [자판기] 자판기가 보유하고 있는 동전의 개수를 출력하는 기능
+
+
+- [x] [자판기] 사용자로부터 상품명, 가격, 수량을 입력받고, 상품을 추가하는 기능
+  - [x] [예외 상황] 개별 상품이 대괄호([])로 묶이지 않는 경우
+  - [x] [예외 상황] 상품명, 가격, 수량이 쉼표(,)로 구분되지 않는 경우
+  - [x] [예외 상황] 상품 가격이 100원부터 시작하지 않고, 10원으로 나누어 떨어지지지 않는 경우
+  - [x] [예외 상황] 상품명이 비어있는 경우
+  - [x] [예외 상황] 상품 수량이 0 미만인 경우
+
+
+- [x] [구매] 사용자로부터 투입 금액을 입력받는 기능
+
+
+- [x] [구매] 사용자로부터 구매할 상품명을 입력받는 기능
+
+
+- [x] [구매] 입력받은 투입 금액과 상품명을 이용해 구매를 진행하는 기능
+
+
+- [x] [잔돈 반환] 잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려주는 기능
+  - [x] 지폐를 잔돈으로 반환하는 경우는 없다고 가정한다.
+  - [x] 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다.
+  - [x] 남은 금액이 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다.
+  - [x] 반환되지 않은 금액은 자판기에 남는다.
+
+
+과제 시작 시간: 21:30
+<br>TDD 종료 : 01:58 (4시간 28분)
+<br>MVP 완성 : 02:13 (TDD + 15분)
+<br>5시간 종료 : 02:30
+<br>리팩토링 종료 : 03:10 (MVP 완성 + 40분)
+<br>총 5시간 40분

리뷰: 넵 무작정 구현에 돌입하는게 어려워서, 머릿속으로 계속 생각하면서 구현했습니다!
@@ -165,3 +165,47 @@ public enum Coin {
 - **Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위**로 추가한다.
    - [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남긴다.
 - 과제 진행 및 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고한다.
+
+---
+
+## 기능 구현 목록
+- [x] [자판기] 사용자로부터 자판기가 보유하고 있는 금액을 입력받는 기능
+
+
+- [x] [자판기] 자판기가 보유하고 있는 금액만큼 동전을 무작위로 생성하는 기능
+  - [x] 투입 금액으로는 동전을 생성하지 않는다.
+
+
+- [x] [자판기] 자판기가 보유하고 있는 동전의 개수를 출력하는 기능
+
+
+- [x] [자판기] 사용자로부터 상품명, 가격, 수량을 입력받고, 상품을 추가하는 기능
+  - [x] [예외 상황] 개별 상품이 대괄호([])로 묶이지 않는 경우
+  - [x] [예외 상황] 상품명, 가격, 수량이 쉼표(,)로 구분되지 않는 경우
+  - [x] [예외 상황] 상품 가격이 100원부터 시작하지 않고, 10원으로 나누어 떨어지지지 않는 경우
+  - [x] [예외 상황] 상품명이 비어있는 경우
+  - [x] [예외 상황] 상품 수량이 0 미만인 경우
+
+
+- [x] [구매] 사용자로부터 투입 금액을 입력받는 기능
+
+
+- [x] [구매] 사용자로부터 구매할 상품명을 입력받는 기능
+
+
+- [x] [구매] 입력받은 투입 금액과 상품명을 이용해 구매를 진행하는 기능
+
+
+- [x] [잔돈 반환] 잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려주는 기능
+  - [x] 지폐를 잔돈으로 반환하는 경우는 없다고 가정한다.
+  - [x] 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다.
+  - [x] 남은 금액이 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다.
+  - [x] 반환되지 않은 금액은 자판기에 남는다.
+
+
+과제 시작 시간: 21:30
+<br>TDD 종료 : 01:58 (4시간 28분)
+<br>MVP 완성 : 02:13 (TDD + 15분)
+<br>5시간 종료 : 02:30
+<br>리팩토링 종료 : 03:10 (MVP 완성 + 40분)
+<br>총 5시간 40분

리뷰: 새벽 코딩 쉽진 않지만, 집중 하나는 잘 되네요🙂
@@ -0,0 +1,25 @@
+package vendingmachine.constant;
+
+public enum ErrorMessage {
+    INVALID_COIN_MONEY("[ERROR] 유효하지 않은 동전 금액입니다."),
+    INVALID_PRODUCT_NAME("[ERROR] 상품명은 비어 있을 수 없습니다."),
+    INVALID_PRODUCT_PRICE("[ERROR] 상품 가격은 100원부터 시작하며 10원으로 나누어 떨어져야 합니다."),
+    INVALID_PRODUCT_QUANTITY("[ERROR] 상품 수량은 0 이상이어야 합니다."),
+    PRODUCT_SOLD_OUT("[ERROR] 해당 상품은 품절되었습니다."),
+    PRODUCT_IS_NOT_EXIST("[ERROR] 해당 상품이 존재하지 않습니다."),
+    NOT_ENOUGH_MONEY("[ERROR] 구매 가능한 상품이 없습니다."),
+    MONEY_SHOULD_BE_NUMBER("[ERROR] 금액은 숫자여야 합니다."),
+    MONEY_SHOULD_BE_POSITIVE("[ERROR] 금액은 양수여야 합니다."),
+    INVALID_PRODUCT_FORMAT("[ERROR] 상품의 형식이 올바르지 않습니다."),
+    ;
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: 좋은 것 같아요! 피드백 감사합니다:)
@@ -0,0 +1,25 @@
+package vendingmachine.constant;
+
+public enum ErrorMessage {
+    INVALID_COIN_MONEY("[ERROR] 유효하지 않은 동전 금액입니다."),
+    INVALID_PRODUCT_NAME("[ERROR] 상품명은 비어 있을 수 없습니다."),
+    INVALID_PRODUCT_PRICE("[ERROR] 상품 가격은 100원부터 시작하며 10원으로 나누어 떨어져야 합니다."),
+    INVALID_PRODUCT_QUANTITY("[ERROR] 상품 수량은 0 이상이어야 합니다."),
+    PRODUCT_SOLD_OUT("[ERROR] 해당 상품은 품절되었습니다."),
+    PRODUCT_IS_NOT_EXIST("[ERROR] 해당 상품이 존재하지 않습니다."),
+    NOT_ENOUGH_MONEY("[ERROR] 구매 가능한 상품이 없습니다."),
+    MONEY_SHOULD_BE_NUMBER("[ERROR] 금액은 숫자여야 합니다."),
+    MONEY_SHOULD_BE_POSITIVE("[ERROR] 금액은 양수여야 합니다."),
+    INVALID_PRODUCT_FORMAT("[ERROR] 상품의 형식이 올바르지 않습니다."),
+    ;
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: 조금 더 꼼꼼히 샬펴봐야겠네요 피드백 감사합니다!
@@ -0,0 +1,21 @@
+package vendingmachine.constant;
+
+public enum OutputMessage {
+    INPUT_MACHINE_HAVE("자판기가 보유하고 있는 금액을 입력해 주세요."),
+    MACHINE_HAVE("\n자판기가 보유한 동전"),
+    INPUT_PRODUCTS("\n상품명과 가격, 수량을 입력해 주세요."),
+    INPUT_MONEY("\n투입 금액을 입력해 주세요."),
+    BUYING("\n투입 금액: %d원%n구매할 상품명을 입력해주세요."),
+    RETURN_CHANGE("\n잔돈 반환:"),
+    ;
+
+    private final String message;
+
+    OutputMessage(String message) {
+        this.message = message;
+    }
+
+    public String format(Object... args) {
+        return String.format(this.message, args);
+    }
+}

리뷰: 엇 마음이 급했는지 \n과 %n을 혼용했네요! %n 활용해볼게요!!