ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,76 @@
+package store.model;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+
+import java.text.NumberFormat;
+import java.time.LocalDateTime;
+
+public class Product {
+    private final String name;
+    private int price;
+    private int quantity;
+    private Promotion promotion;
+
+    public Product(String name, int price, int quantity, Promotion promotion) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int addPromotions(int number) {
+        int buy = promotion.getBuy();
+        int get = promotion.getGet();
+        if (number % (buy + get) >= buy) {
+            return get - (number % (buy + get) - buy);
+        }
+        return 0;
+    }
+
+    public int applyPromotions(int number) {
+        int buy = promotion.getBuy();
+        int get = promotion.getGet();
+        return (number / (get + buy)) * get;
+    }
+
+    public boolean isPromotion() {
+        if (promotion == null) {
+            return false;
+        }
+        LocalDateTime time = DateTimes.now();
+        if (time.isBefore(promotion.getStartDate()) || time.isAfter(promotion.getEndDate())) {
+            return false;
+        }
+        return true;
+    }
+
+    public void setQuantity(int quantity) {
+        this.quantity = quantity;
+    }
+
+    public int getPromotionBuy() {
+        return promotion.getBuy();
+    }
+
+    @Override
+    public String toString() {
+        NumberFormat formatter = NumberFormat.getInstance();
+        StringBuilder result = new StringBuilder("- " + name + " " + formatter.format(price) + "원");
+        if (quantity > 0) result.append(" ").append(quantity).append("개");
+        if (quantity == 0) result.append(" 재고 없음");
+        if (promotion != null) result.append(" ").append(promotion.getName());
+        return result.toString();
+    }
+}

리뷰: 필드 값 중 quantitiy는 아래 set메서드가 있어서 가변으로 하는건 이해하겠는데
price, promotion은 final로 불변으로 만드는게 좋은거 같다는 생각이 듭니다.

혹시 가변으로 하신 어떤 의도가 있으신가요?
@@ -0,0 +1,77 @@
+package store.controller;
+
+import store.model.Product;
+import store.model.Promotion;
+import store.model.Store;
+import store.view.OutputView;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class StoreController {
+
+    public void run() {
+        Store store = new Store();
+        BuyController buyController = new BuyController(store);
+        promotionListUp(store);
+        productListUp(store);
+        OutputView.printInventoryInformation(store);
+        buyController.buyProduct();
+    }
+
+    private void promotionListUp(Store store) {
+        String productsContent = readMarkdownFile("promotions.md");
+        String[] lines = productsContent.split("\n");
+        Arrays.stream(lines)
+                .map(this::parsePromotion)
+                .filter(Objects::nonNull)
+                .forEach(store::addPromotion);
+    }
+
+    private Promotion parsePromotion(String line) {
+        String[] parts = line.split(",");
+        String name = parts[0];
+        if (name.equals("name")) return null;
+        int buy = Integer.parseInt(parts[1]);
+        int get = Integer.parseInt(parts[2]);
+        String startDate = parts[3];
+        String endDate = parts[4];
+        return new Promotion(name, buy, get, startDate, endDate);
+    }
+
+    private void productListUp(Store store) {
+        String productsContent = readMarkdownFile("products.md");
+        String[] lines = productsContent.split("\n");
+        Arrays.stream(lines)
+                .map(line -> parseProduct(line, store))
+                .filter(Objects::nonNull)
+                .forEach(store::addProduct);
+    }
+
+    private Product parseProduct(String line, Store store) {
+        String[] parts = line.split(",");
+        String name = parts[0];
+        if (name.equals("name")) return null;
+        int price = Integer.parseInt(parts[1]);
+        int quantity = Integer.parseInt(parts[2]);
+        Promotion promotion = store.findPromotionByName(parts[3]);
+        return new Product(name, price, quantity, promotion);
+    }
+
+    private String readMarkdownFile(String fileName) {
+        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName);
+        if (inputStream == null) {
+            throw new IllegalArgumentException("[ERROR] 재고 파일을 찾을 수 없습니다.");
+        }
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+            return reader.lines().collect(Collectors.joining("\n"));
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}

리뷰: 코드 작성할 때는 간결해 보였는데 막상 리뷰 받으니까 중괄호작성할 때는 간결해 보였는데 막상 리뷰 받으니까 중괄호를 넣는게 더 좋아보이네요 😢
@@ -0,0 +1,46 @@
+package store.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+public class Store {
+    private final List<Product> productList = new ArrayList<>();
+    private final List<Promotion> promotionList = new ArrayList<>();
+
+    public void addProduct(Product product) {
+        productList.add(product);
+    }
+
+    public List<Product> getProductList() {
+        return productList;
+    }
+
+    public void addPromotion(Promotion promotion) {
+        promotionList.add(promotion);
+    }
+
+    public List<Promotion> getPromotionList() {
+        return promotionList;
+    }
+
+    public Promotion findPromotionByName(String name) {
+        return promotionList.stream()
+                .filter(promo -> promo.getName().equals(name))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public void deductInventory(String productName, int quantity) {
+        productList.stream()
+                .filter(product -> product.getName().equals(productName))
+                .findFirst()
+                .ifPresent(product -> product.setQuantity(product.getQuantity() - quantity));
+    }
+
+    public Optional<Product> findProductByName(String productName) {
+        return productList.stream()
+                .filter(product -> product.getName().equals(productName))
+                .findFirst();
+    }
+}

리뷰: 리팩토링이 부족했던 것 같습니다.
개인적으로 기본적인 게터는 남겨둬도 크게 상관 없다고 생각은 합니다 🤔
@@ -0,0 +1,76 @@
+package store.model;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+
+import java.text.NumberFormat;
+import java.time.LocalDateTime;
+
+public class Product {
+    private final String name;
+    private int price;
+    private int quantity;
+    private Promotion promotion;
+
+    public Product(String name, int price, int quantity, Promotion promotion) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int addPromotions(int number) {
+        int buy = promotion.getBuy();
+        int get = promotion.getGet();
+        if (number % (buy + get) >= buy) {
+            return get - (number % (buy + get) - buy);
+        }
+        return 0;
+    }
+
+    public int applyPromotions(int number) {
+        int buy = promotion.getBuy();
+        int get = promotion.getGet();
+        return (number / (get + buy)) * get;
+    }
+
+    public boolean isPromotion() {
+        if (promotion == null) {
+            return false;
+        }
+        LocalDateTime time = DateTimes.now();
+        if (time.isBefore(promotion.getStartDate()) || time.isAfter(promotion.getEndDate())) {
+            return false;
+        }
+        return true;
+    }
+
+    public void setQuantity(int quantity) {
+        this.quantity = quantity;
+    }
+
+    public int getPromotionBuy() {
+        return promotion.getBuy();
+    }
+
+    @Override
+    public String toString() {
+        NumberFormat formatter = NumberFormat.getInstance();
+        StringBuilder result = new StringBuilder("- " + name + " " + formatter.format(price) + "원");
+        if (quantity > 0) result.append(" ").append(quantity).append("개");
+        if (quantity == 0) result.append(" 재고 없음");
+        if (promotion != null) result.append(" ").append(promotion.getName());
+        return result.toString();
+    }
+}

리뷰: 상품을 구분하는 유일한 방법이 상품이름이여서 name 빼고는 바뀔 수도 있다고 생각했었습니다.
현재 코드를 기준으로는 quantitiy 빼고는 final 처리를 해줘도 된다고 생각합니다.
@@ -0,0 +1,54 @@
+package store.domain.promotion;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+
+/**
+ * Promotion 은 프로모션 정보를 필드로 가지고 있습니다.
+ * 프로모션 할인 가격 등 다양한 프로모션 정보를 제공하는 것이 책임입니다.
+ */
+public class Promotion {
+    private final String promotionName;
+    private final int requiredBuyCount;
+    private final int toGiveItemCount;
+    private final Period period;
+
+    public Promotion(PromotionParameter promotionParameter) {
+        this.promotionName = promotionParameter.getPromotionName();
+        this.requiredBuyCount = promotionParameter.getRequiredBuyCount();
+        this.toGiveItemCount = promotionParameter.getToGiveItemCount();
+        this.period = promotionParameter.getPeriod();
+    }
+
+    /* 프로모션을 적용할 수 있는 상품 개수를 구하는 법
+     * 콜라 2+1 프로모션 상품이 7개 있을 때, 사용자가 프로모션을 적용할 수 있는 콜라의 최대 개수는 6개다. 4개를 구매하고, 2개를 무료로 받으면 총 6개이기 때문이다.
+     * 5개를 구매하고 2+1 이벤트를 최대한 적용하여 최대 7개를 얻는 것도 가능하다. 하지만 사용자가 프로모션 재고로는 처리가 불가능한 수량을 입력했을 경우,
+     * 우테코에서 제시한 실행 결과 예시에 따르면 정가로 구매해야 하는 콜라의 개수는 (사용자가 입력한 수량) - (최대 적용 가능한 프로모션 수량) 이기 때문에
+     * 정가로 구매하는 상품을 제외하는 옵션을 택할 시 프로모션 재고가 몇 개 남을수도 있다.
+     *
+     * 따라서, 프로모션 재고 수가 N일때 프로모션이 적용될 수 있는 최대 개수는 [N - (N % (requiredBuyCount + toGiveItemCount))] 이다.
+     */
+    public int getMaxAvailablePromotionQuantity(int currentStockCount) {
+        return currentStockCount - (currentStockCount % (requiredBuyCount + toGiveItemCount));
+    }
+
+    public String getPromotionName() {
+        return promotionName;
+    }
+
+    public int getRequiredBuyCount() {
+        return requiredBuyCount;
+    }
+
+    public boolean isAvailablePromotion() {
+        return period.isBetweenPeriod(DateTimes.now());
+    }
+
+    public int getDiscountPrice(int buyQuantity, int regularPrice) {
+        int freeItemCount = buyQuantity / (requiredBuyCount + toGiveItemCount);
+        return freeItemCount * regularPrice;
+    }
+
+    public int getDiscountedQuantity(int buyQuantity) {
+        return buyQuantity / (requiredBuyCount + toGiveItemCount);
+    }
+}

리뷰: 주석을 많이 달아주시는 건 정말 좋은 점이지만, 전체적으로 조금만 줄여보는 건 어떨까요? 
사실, 코드가 더 간결하고 명확해지면 주석 없이도 그 의도가 잘 전달될 수 있어요 😊 
예외적인 상황이나 복잡한 로직에서만 간단한 주석을 추가하는 게 더 효과적일 것 같아요 !
@@ -0,0 +1,25 @@
+package store.messages;
+
+public enum ErrorMessage {
+    INVALID_INPUT("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요."),
+    INVALID_INPUT_OTHER("잘못된 입력입니다. 다시 입력해 주세요."),
+    INVALID_PROMOTION_FILE_PARAMETERS("promotions.md의 파라미터 형식이 올바르지 않습니다."),
+    INVALID_PROMOTION_FILE_FORM("올바르지 않은 promotions.md 형식입니다."),
+    INVALID_PRODUCT_FILE_PARAMETERS("products.md의 파라미터 형식이 올바르지 않습니다."),
+    INVALID_PRODUCT_FILE_FORM("올바르지 않은 products.md 형식입니다."),
+    INVALID_PRODUCT_NAME("존재하지 않는 상품입니다. 다시 입력해 주세요."),
+    INVALID_PRODUCT_PROMOTIONS("products.md 파일의 형식에 문제가 있습니다. 동일 상품에 여러 프로모션이 적용될 수 없습니다."),
+    OVER_STOCK("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    FILE_NOT_FOUND("편의점 구성 파일을 찾을 수 없습니다. 프로그램을 종료합니다.");
+
+    private static final String ERROR_PREFIX = "[ERROR] ";
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return ERROR_PREFIX + message;
+    }
+}

리뷰: `다시 입력해 주세요.`, `(Y/N)` 같은 부분도 상수로 따로 빼면 유지보수에 더 좋을 것 같아요 !
@@ -0,0 +1,79 @@
+package store.util;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Scanner;
+import store.domain.Stock;
+import store.domain.promotion.Promotion;
+import store.domain.promotion.PromotionParameter;
+import store.domain.promotion.Promotions;
+import store.domain.product.Product;
+import store.domain.product.ProductParameter;
+
+/**
+ * StoreInitializer 는 상품과 프로모션 파일을 읽고, 재고 정보를 생성하는 유틸성 클래스입니다.
+ */
+public class StoreInitializer {
+    private Promotions promotions;
+
+    public Stock initStock() throws FileNotFoundException {
+        promotions = readPromotionFile();
+        Stock stock = readProductFile();
+        stock.generateNormalProductFromOnlyPromotionProduct();
+        return stock;
+    }
+
+    private Promotions readPromotionFile() throws FileNotFoundException {
+        Scanner scanner = new Scanner(new File("src/main/resources/promotions.md"));
+        Validator.checkPromotionFirstLine(scanner.nextLine());
+        return readPromotionLines(scanner);
+    }
+
+    private Promotions readPromotionLines(Scanner scanner) {
+        List<Promotion> promotions = new ArrayList<>();
+        while (scanner.hasNext()) {
+            String currentLine = scanner.nextLine();
+            promotions.add(initPromotionThat(currentLine));
+        }
+        return new Promotions(promotions);
+    }
+
+    private Promotion initPromotionThat(String line) {
+        Validator.checkPromotionInitLine(line);
+        List<String> parameters = List.of(line.split(","));
+        PromotionParameter promotionParameter = new PromotionParameter(parameters);
+        return new Promotion(promotionParameter);
+    }
+
+    private Stock readProductFile() throws FileNotFoundException {
+        Scanner scanner = new Scanner(new File("src/main/resources/products.md"));
+        Validator.checkProductFirstLine(scanner.nextLine());
+        return readProductLines(scanner);
+    }
+
+    private Stock readProductLines(Scanner scanner) {
+        List<Product> products = new ArrayList<>();
+        while (scanner.hasNext()) {
+            String currentLine = scanner.nextLine();
+            products.add(initProductThat(currentLine));
+        }
+
+        return new Stock(products);
+    }
+
+    private Product initProductThat(String line) {
+        Validator.checkProductInitLine(line);
+        List<String> parameters = List.of(line.split(","));
+        ProductParameter productParameter = new ProductParameter(parameters);
+        Promotion promotion = findPromotionByName(productParameter.getPromotionName());
+        return new Product(productParameter, promotion);
+    }
+
+    private Promotion findPromotionByName(String promotionName) {
+        Optional<Promotion> promotion = promotions.findPromotionByName(promotionName);
+        return promotion.orElse(null);
+    }
+}

리뷰: 에러를 직접 처리하지 않고, 던져서 main에서 처리한 이유가 있으실까요!? 궁금합니다!
@@ -0,0 +1,79 @@
+package store.util;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Scanner;
+import store.domain.Stock;
+import store.domain.promotion.Promotion;
+import store.domain.promotion.PromotionParameter;
+import store.domain.promotion.Promotions;
+import store.domain.product.Product;
+import store.domain.product.ProductParameter;
+
+/**
+ * StoreInitializer 는 상품과 프로모션 파일을 읽고, 재고 정보를 생성하는 유틸성 클래스입니다.
+ */
+public class StoreInitializer {
+    private Promotions promotions;
+
+    public Stock initStock() throws FileNotFoundException {
+        promotions = readPromotionFile();
+        Stock stock = readProductFile();
+        stock.generateNormalProductFromOnlyPromotionProduct();
+        return stock;
+    }
+
+    private Promotions readPromotionFile() throws FileNotFoundException {
+        Scanner scanner = new Scanner(new File("src/main/resources/promotions.md"));
+        Validator.checkPromotionFirstLine(scanner.nextLine());
+        return readPromotionLines(scanner);
+    }
+
+    private Promotions readPromotionLines(Scanner scanner) {
+        List<Promotion> promotions = new ArrayList<>();
+        while (scanner.hasNext()) {
+            String currentLine = scanner.nextLine();
+            promotions.add(initPromotionThat(currentLine));
+        }
+        return new Promotions(promotions);
+    }
+
+    private Promotion initPromotionThat(String line) {
+        Validator.checkPromotionInitLine(line);
+        List<String> parameters = List.of(line.split(","));
+        PromotionParameter promotionParameter = new PromotionParameter(parameters);
+        return new Promotion(promotionParameter);
+    }
+
+    private Stock readProductFile() throws FileNotFoundException {
+        Scanner scanner = new Scanner(new File("src/main/resources/products.md"));
+        Validator.checkProductFirstLine(scanner.nextLine());
+        return readProductLines(scanner);
+    }
+
+    private Stock readProductLines(Scanner scanner) {
+        List<Product> products = new ArrayList<>();
+        while (scanner.hasNext()) {
+            String currentLine = scanner.nextLine();
+            products.add(initProductThat(currentLine));
+        }
+
+        return new Stock(products);
+    }
+
+    private Product initProductThat(String line) {
+        Validator.checkProductInitLine(line);
+        List<String> parameters = List.of(line.split(","));
+        ProductParameter productParameter = new ProductParameter(parameters);
+        Promotion promotion = findPromotionByName(productParameter.getPromotionName());
+        return new Product(productParameter, promotion);
+    }
+
+    private Promotion findPromotionByName(String promotionName) {
+        Optional<Promotion> promotion = promotions.findPromotionByName(promotionName);
+        return promotion.orElse(null);
+    }
+}

리뷰: 사소한거긴 하지만 `","` 이거보다는 상수로 분리해 어떤 구분자인지 표현하는 것도 좋을 거 같아요!
@@ -1 +1,90 @@
 # java-convenience-store-precourse
+## 프로젝트 설명
+구매자의 할인 혜택과 재고 상황을 고려하여 최종 결제 금액을 계산하고 안내하는 결제 시스템을 구현하는 과제입니다.
+
+사용자가 입력한 상품별 가격과 수량을 곱하여 계산하고, 최종 결제 금액을 계산하여 영수증을 출력해야 합니다.  
+각 상품의 재고를 고려하여 결제 가능 여부를 파악해야 하고, 프로모션이 적용되어 있는 상품들이 있습니다.  
+프로모션은 N개 구매 시 1개 무료 증정의 형태로 진행되고, 오늘 날짜가 프로모션 기간 내라면 할인을 적용합니다.  
+멤버십 회원은 프로모션이 적용되지 않은 금액의 30%를 할인받습니다. 최대 할인 한도는 8,000원입니다.  
+영수증은 고객의 구매 내역과 할인을 요약하여 보기 좋게 출력해야 합니다.
+
+<br>
+
+## 기능 요구 사항에 기재되지 않아서 스스로 판단한 내용
+이번 과제는 기능 요구 사항에 기재되지 않은 애매한 내용들이 꽤 있었습니다.  
+이에 대해 스스로 판단한 요구 사항을 알려드립니다.  
+
+1. 프로모션 기간이 아직 시작되지 않았거나 이미 종료되었다면, 해당 상품은 프로모션이 적용되지 않은 일반 상품으로 취급되는 것이지 재고 자체가 사라지진 않습니다. 즉 일반 재고로 취급합니다.
+2. 멤버십 회원은 프로모션 재고 상품의 총 구매가를 제외한 구매 비용의 30%를 할인받습니다.
+3. 프로모션 2+1 콜라가 7개 있고, 사용자가 7개를 구매하길 원한다면 실제로 프로모션을 적용받을 수 있는 콜라는 6개까지이지만, 프로모션 재고가 부족한 것은 아니기에 7개를 모두 구매할 수 있습니다. (우아한테크코스의 예시를 참고하면, 프로모션 재고가 부족하여 일반 재고까지 구매해야 하는 상황에서만 프로모션이 미적용 되는 상품에 대해서 안내를 합니다.  예를 들어서 프로모션 2+1 콜라가 10개 있고, 일반 재고의 콜라가 10개 있는 상황에서 사용자가 12개를 구매하려고 한다면, 프로모션을 적용할 수 있는 실제 재고 개수는 9개 이므로 3개에 대해 프로모션 미적용 안내를 합니다)
+4. 프로모션이 적용된 모든 상품은 프로모션이 적용되지 않은 버전의 상품 정보도 표시해야 하고, 프로모션 미적용 상품이 존재하지 않는다면 "재고 없음" 을 표시해야 합니다.
+
+<br>
+
+## 설계 과정
+객체지향 설계에 있어서 가장 중요한 것은 협력을 위해 어떤 책임과 역할이 필요한지 이해하는 것이라고 생각합니다.  
+따라서 설계의 방향을 잡기 위해 필요한 역할과 책임 그리고 메시지를 정리해 봤습니다.  
+이후 정리한 내용을 통해 도출한 대략적인 그림을 참고해서 구현할 기능 목록을 하단에 요약했습니다.
+
+
+> 이해한 과제 내용을 대략적으로 정리하는 것은 필요한 역할과 책임을 이해하는 데에 도움이 된다고 생각합니다.  
+> 이렇게 정리한 내용에서 영감을 받아 구현할 기능 목록을 대략적으로나마 작성할 수 있었습니다.  
+> 물론 구현 중 계획했던 내용이 틀어질 수 있지만, 프로젝트를 시작하는 데에 도움이 되었습니다.
+
+<br>
+
+### 필요한 역할 및 기능
+- [x] 상품의 정보(상품명과 가격)을 가지고 있는 역할이 있어야 한다.
+- [x] 상품별 재고 정보를 가지고 있는 역할이 있어야 한다.
+- [x] 재고 수량을 고려하여 결제 가능 여부를 확인할 수 있어야 한다.
+- [x] 상품이 구입될 때마다, 결제된 수량만큼 해당 상품의 재고에서 차감해야 한다.
+- [x] 상품에 프로모션을 적용할 수 있어야 한다. 오늘 날짜가 프로모션 기간인 경우에만 할인을 적용해야 한다.
+- [x] 프로모션 기간 중이라면 프로모션 재고를 우선적으로 차감해야 한다.
+- [x] 멤버쉽 회원은 프로모션 미적용 금액의 30%를 할인받는다.
+- [x] 멤버쉽 할인의 최대 한도는 8,000원이다.
+- [x] 영수증 기능이 필요하다.
+
+
+### 메시지 정리
+- [x] 환영 문구를 출력하라 -> (출력 역할) -> OutputView  
+- [x] 사용자에게 상품의 가격과 수량을 입력받아라 -> (입력 역할) -> InputView  
+- [x] 상품 재고를 초기화하라 -> (파일 입출력해서 재고 초기화하는 역할) -> StoreInitializer  
+- [x] 상품 재고를 출력하라 -> OutputView -> (상품 재고를 갖고있는 역할) -> StoreStock  
+- [x] 결제 가능 여부를 파악하라 -> (상품 재고를 갖고있는 역할) -> StoreStock  
+- [x] 프로모션 상품인지 확인하라 -> (상품 역할) -> StoreItem  
+- [x] 프로모션 재고가 충분한지 파악하라 -> (상품 역할) -> StoreItem
+
+
+<br>
+
+
+## 구현할 기능 목록
+### 1. 출력 역할
+- [x] 출력 역할의 객체를 만든다
+- [x] 환영 문구를 출력하는 기능
+- [x] 재고 정보를 출력하는 기능
+- [x] 프롬프트 메시지를 출력하는 기능
+- [x] 영수증 내용을 출력하는 기능
+
+### 2. 입력 역할
+- [x] 입력 역할의 객체를 만든다
+- [x] 사용자에게 구매할 상품명과 수량을 입력받는 기능
+- [x] Y/N 형태로 입력받는 기능
+- [x] 입력 값이 잘못되었을 경우 오류 메시지와 함께 재입력 받는 기능
+
+### 3. 파일 입출력 역할
+- [x] 파일 입출력 역할의 객체를 만든다
+- [x] .md 파일을 읽어서 상품 재고를 초기화하는 기능
+
+### 4. 상품 역할
+- [x] 상품의 정보(상품명과 가격 등)을 가지고 있는 객체를 만든다
+- [x] 상품에 프로모션 정보를 포함할 수 있는 기능을 만든다
+
+### 5. 재고 역할
+- [x] 상품의 정보들을 가지고, 재고 역할을 하는 객체를 만든다
+- [x] 재고에서 특정 상품을 찾는 기능
+- [x] 프로모션 상품인지, 비 프로모션 상품인지 구분하는 기능
+
+### 6. 영수증 역할
+- [x] 영수증 역할의 객체를 만든다
+- [x] 구입 내역을 입력받아서 저장하는 기능
\ No newline at end of file

리뷰: 궁금한 점이 있습니다! 프로그램 로직을 구성할 때, 상세한 명세서가 중요하다고 생각합니다. 혹시, 작성하신 문서로도 충분한 프로그램 구성에 문제가 없으신가요!? 괜찮으시다면, 문제 접근 방법이 궁금합니다. 보고 배우고 싶어서.. 하하 🤣
@@ -0,0 +1,63 @@
+package store;
+
+import store.domain.Stock;
+import store.domain.receipt.Receipt;
+import store.domain.order.OrderItems;
+import store.view.View;
+
+public class StoreController {
+    private final StoreService storeService;
+    private final Stock stock;
+
+    public StoreController(StoreService storeService, Stock stock) {
+        this.storeService = storeService;
+        this.stock = stock;
+    }
+
+    public void run() {
+        do {
+            runStoreProcess();
+        } while (askContinueShopping());
+    }
+
+    private void runStoreProcess() {
+        printGreetingMessage();
+        printStockStatus(stock);
+        OrderItems orderItems = getOrderItems();
+        try {
+            Receipt receipt = proceedPurchase(orderItems);
+            printReceipt(receipt);
+        } catch (Exception e) {
+            printErrorMessage(e.getMessage());
+        }
+    }
+
+    private void printGreetingMessage() {
+        View.getInstance().printGreetingMessage();
+    }
+
+    private void printStockStatus(Stock stock) {
+        View.getInstance().printStockStatus(stock.getProducts());
+    }
+
+    private OrderItems getOrderItems() {
+        String input = View.getInstance().promptBuyItems();
+        return storeService.getOrderItems(input);
+    }
+
+    private Receipt proceedPurchase(OrderItems orderItems) {
+        return storeService.proceedPurchase(stock, orderItems);
+    }
+
+    private void printReceipt(Receipt receipt) {
+        View.getInstance().printReceipt(receipt);
+    }
+
+    private boolean askContinueShopping() {
+        return View.getInstance().promptContinueShopping();
+    }
+
+    private void printErrorMessage(String errorMessage) {
+        View.getInstance().printErrorMessage(errorMessage);
+    }
+}

리뷰: 진짜 컨트롤러 너무 깔끔한 거 같아요. 감탄합니다.. ㅠ 보고 배웁니다.. 👍
@@ -0,0 +1,35 @@
+package store.domain.order;
+
+/**
+ * OrderItem 은 사용자의 주문 내역(주문한 상품 이름, 수량)을 필드로 가지고 있습니다.
+ * 사용자의 주문 내역을 관리하는 것이 책임입니다.
+ */
+public class OrderItem {
+    private final String itemName;
+    private int quantity;
+
+    public OrderItem(String itemName, int quantity) {
+        this.itemName = itemName;
+        this.quantity = quantity;
+    }
+
+    public String getItemName() {
+        return itemName;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public void addQuantity(int amount) {
+        if (amount > 0) {
+            quantity += amount;
+        }
+    }
+
+    public void subQuantity(int amount) {
+        if (amount > 0) {
+            quantity -= amount;
+        }
+    }
+}

리뷰: 저도 이부분이 큰 고민인데, 공통 피드백에 메시지의 의미가 없는 get, set 사용을 자제하는 내용이 있었습니다. 이런 경우와는 다른 내용일까요!?,, 저도 고민이 깊어서 ㅠ