ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,63 @@
+package store.custom.model.order;
+
+public class OrderedProduct {
+    private final String name;
+    private int quantity;
+    private int totalPrice;
+    private String promotion;
+    private int buy;
+    private int get;
+
+    public OrderedProduct(String name, int quantity, int totalPrice, String promotion, int buy, int get) {
+        this.name = name;
+        this.quantity = quantity;
+        this.totalPrice = totalPrice;
+        this.promotion = promotion;
+        this.buy = buy;
+        this.get = get;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public void setQuantity(int quantity) {
+        this.quantity = quantity;
+    }
+
+    public int getTotalPrice() {
+        return totalPrice;
+    }
+
+    public void setTotalPrice(int totalPrice) {
+        this.totalPrice = totalPrice;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+
+    public void setPromotion(String promotion) {
+        this.promotion = promotion;
+    }
+
+    public int getBuy() {
+        return buy;
+    }
+
+    public void setBuy(int buy) {
+        this.buy = buy;
+    }
+
+    public int getGet() {
+        return get;
+    }
+
+    public void setGet(int get) {
+        this.get = get;
+    }
+}
\ No newline at end of file

리뷰: 객체에 대한 공부를 더 해봐야겠습니다! 알려주셔서 감사합니다!
@@ -0,0 +1,35 @@
+package store.custom.model.product;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int quantity;
+    private final String promotion;
+
+    public Product(String name, int price, int quantity, String promotion) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public void setQuantity(int quantity) {
+        this.quantity = quantity;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+}
\ No newline at end of file

리뷰: 그 생각을 못해봤네요! 확실히 프로모션 객체를 참조하면 더 좋을 것 같네요!
@@ -0,0 +1,41 @@
+package store.domain;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import store.domain.vo.PromotionType;
+
+public class Promotions {
+
+    private static final int BUY_INDEX = 1;
+    private static final int FREE_GET_INDEX = 2;
+    private static final int START_DATE_INDEX = 3;
+
+    private final List<Promotion> promotions;
+
+    public Promotions(List<List<String>> contents, LocalDateTime currentTime) {
+        this.promotions = makePromotions(contents, currentTime);
+    }
+
+    public Promotion getPromotionByType(PromotionType type) {
+        return promotions.stream()
+                .filter(promotion -> promotion.getPromotionType().equals(type.getPromotionType()))
+                .findFirst()
+                .orElse(Promotion.noPromotion());
+    }
+
+    private List<Promotion> makePromotions(List<List<String>> contents, LocalDateTime currentTime) {
+        List<Promotion> promotions = new ArrayList<>();
+        makePromotion(contents, currentTime, promotions);
+        return promotions;
+    }
+
+    private void makePromotion(List<List<String>> contents, LocalDateTime currentTime, List<Promotion> promotions) {
+        for (List<String> content : contents) {
+            Promotion promotion = Promotion.from(
+                    PromotionType.from(content.getFirst()), currentTime, content.get(BUY_INDEX), content.get(FREE_GET_INDEX), content.get(START_DATE_INDEX), content.getLast()
+            );
+            promotions.add(promotion);
+        }
+    }
+}

리뷰: List<List<String>>으로 정보를 뽑아내는 것은 도메인의 역할이 아닌 view의 역할에 더 가까운 것 같습니다. View에서 어느정도의 데이터를 가공한 후 DTO를 통해 product를 생성하는 것은 어떨까요?
@@ -0,0 +1,60 @@
+package store.domain;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import store.domain.vo.ProductQuantity;
+import store.domain.vo.PromotionType;
+
+public class Promotion {
+
+    private final PromotionType promotionType;
+    private final ProductQuantity buyQuantity;
+    private final ProductQuantity getQuantity;
+    private final boolean isPromotion;
+
+    public static Promotion from(PromotionType promotionType, LocalDateTime currentDate, String buyQuantity, String getQuantity,
+                     String startDate, String endDate) {
+
+        return new Promotion(promotionType, ProductQuantity.from(buyQuantity), ProductQuantity.from(getQuantity), isPromotion(currentDate, startDate, endDate));
+    }
+
+    public static Promotion noPromotion() {
+        return new Promotion(PromotionType.none(), ProductQuantity.none(), ProductQuantity.none(), false);
+    }
+
+    private Promotion(PromotionType promotionType, ProductQuantity buyQuantity, ProductQuantity getQuantity,
+                      boolean isPromotion) {
+        this.promotionType = promotionType;
+        this.buyQuantity = buyQuantity;
+        this.getQuantity = getQuantity;
+        this.isPromotion = isPromotion;
+    }
+
+    public String getPromotionType() {
+        return promotionType.getPromotionType();
+    }
+
+    public boolean isPromotion() {
+        return isPromotion;
+    }
+
+    public int getBuyQuantity() {
+        return buyQuantity.getQuantity();
+    }
+
+    public int getGettableQuantity() {
+        return getQuantity.getQuantity();
+    }
+
+    public int getOnePromotionCycle() {
+        return buyQuantity.getQuantity() + getQuantity.getQuantity();
+    }
+
+    private static boolean isPromotion(LocalDateTime currentDate, String startDate, String endDate) {
+        LocalDateTime start = LocalDate.parse(startDate).atStartOfDay();
+        LocalDateTime end = LocalDate.parse(endDate).atTime(LocalTime.MAX);
+
+        return currentDate.isAfter(start) && currentDate.isBefore(end);
+    }
+}

리뷰: 프로모션 생성 과정을 보면 현재 시간에 맞춰 프로모션 적용을 정하는데 재고를 넣는 시간대와 물품을 사는 시간이 다른 경우 문제가 생길 것 같습니다.
@@ -0,0 +1,76 @@
+package store.view;
+
+import java.util.List;
+import store.view.dto.response.BuyProductResponse;
+import store.view.dto.response.ErrorResponse;
+import store.view.dto.response.FreeProductResponse;
+import store.view.dto.response.ProductCalculateResponse;
+
+public class OutputView {
+
+    private static final String LINE_SEPARATOR = System.lineSeparator();
+
+    public void welcomeStore() {
+        System.out.println(LINE_SEPARATOR + "안녕하세요. W편의점입니다." + LINE_SEPARATOR + "현재 보유하고 있는 상품입니다." + LINE_SEPARATOR);
+    }
+
+    public void showInventory(String name, int price, String quantity, String promotionType) {
+        System.out.printf(
+                "- %s %,d원 %s %s"
+                        + LINE_SEPARATOR,
+                name, price, quantity, promotionType
+        );
+    }
+
+    public void getCustomerProducts() {
+        System.out.println(LINE_SEPARATOR + "구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])");
+    }
+
+    public void guideBuyProducts() {
+        System.out.printf(
+                LINE_SEPARATOR +
+                "==============W 편의점================"
+                        + LINE_SEPARATOR
+                        + "%-10s %9s %9s%n"
+                , "상품명", "수량" ,"금액"
+        );
+    }
+
+    public void showBuyProducts(List<BuyProductResponse> buyProducts) {
+        for (BuyProductResponse product : buyProducts) {
+            System.out.printf(
+                    "%-10s %9d %,14d" + LINE_SEPARATOR,
+                    product.name(), product.quantity(), product.totalPrice()
+            );
+        }
+    }
+
+    public void guideFreeGetQuantity() {
+        System.out.println("=============증\t\t정===============");
+    }
+
+    public void showFreeQuantity(List<FreeProductResponse> freeProductResponses) {
+        for (FreeProductResponse freeProductResponse : freeProductResponses) {
+            System.out.printf(
+                    "%-10s %9d" + LINE_SEPARATOR,
+                    freeProductResponse.name(), freeProductResponse.quantity()
+            );
+        }
+    }
+
+    public void showCalculateResult(ProductCalculateResponse calculateInfo) {
+        System.out.printf(
+                "====================================" + LINE_SEPARATOR
+                        + "총구매액\t\t\t\t%d\t\t  %,d" + LINE_SEPARATOR
+                        + "행사할인\t\t\t\t\t\t  -%,d" + LINE_SEPARATOR
+                        + "멤버십할인\t\t\t\t\t\t  -%,d" + LINE_SEPARATOR
+                        + "내실돈\t\t\t\t\t\t  %,d" + LINE_SEPARATOR
+                , calculateInfo.buyCounts(), calculateInfo.buyProductsPrice(),
+                 calculateInfo.promotionPrice(), calculateInfo.memberShip(), calculateInfo.totalPrice()
+        );
+    }
+
+    public void showError(ErrorResponse errorResponse) {
+        System.out.println(errorResponse.message());
+    }
+}

리뷰: format을 상수를 통해 처리하는 것은 어떤가요?
만약 출력 형식이 좀 달라진다면 수정이 더욱 번거로울것 같습니다.
@@ -0,0 +1,76 @@
+package store.view;
+
+import java.util.List;
+import store.view.dto.response.BuyProductResponse;
+import store.view.dto.response.ErrorResponse;
+import store.view.dto.response.FreeProductResponse;
+import store.view.dto.response.ProductCalculateResponse;
+
+public class OutputView {
+
+    private static final String LINE_SEPARATOR = System.lineSeparator();
+
+    public void welcomeStore() {
+        System.out.println(LINE_SEPARATOR + "안녕하세요. W편의점입니다." + LINE_SEPARATOR + "현재 보유하고 있는 상품입니다." + LINE_SEPARATOR);
+    }
+
+    public void showInventory(String name, int price, String quantity, String promotionType) {
+        System.out.printf(
+                "- %s %,d원 %s %s"
+                        + LINE_SEPARATOR,
+                name, price, quantity, promotionType
+        );
+    }
+
+    public void getCustomerProducts() {
+        System.out.println(LINE_SEPARATOR + "구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])");
+    }
+
+    public void guideBuyProducts() {
+        System.out.printf(
+                LINE_SEPARATOR +
+                "==============W 편의점================"
+                        + LINE_SEPARATOR
+                        + "%-10s %9s %9s%n"
+                , "상품명", "수량" ,"금액"
+        );
+    }
+
+    public void showBuyProducts(List<BuyProductResponse> buyProducts) {
+        for (BuyProductResponse product : buyProducts) {
+            System.out.printf(
+                    "%-10s %9d %,14d" + LINE_SEPARATOR,
+                    product.name(), product.quantity(), product.totalPrice()
+            );
+        }
+    }
+
+    public void guideFreeGetQuantity() {
+        System.out.println("=============증\t\t정===============");
+    }
+
+    public void showFreeQuantity(List<FreeProductResponse> freeProductResponses) {
+        for (FreeProductResponse freeProductResponse : freeProductResponses) {
+            System.out.printf(
+                    "%-10s %9d" + LINE_SEPARATOR,
+                    freeProductResponse.name(), freeProductResponse.quantity()
+            );
+        }
+    }
+
+    public void showCalculateResult(ProductCalculateResponse calculateInfo) {
+        System.out.printf(
+                "====================================" + LINE_SEPARATOR
+                        + "총구매액\t\t\t\t%d\t\t  %,d" + LINE_SEPARATOR
+                        + "행사할인\t\t\t\t\t\t  -%,d" + LINE_SEPARATOR
+                        + "멤버십할인\t\t\t\t\t\t  -%,d" + LINE_SEPARATOR
+                        + "내실돈\t\t\t\t\t\t  %,d" + LINE_SEPARATOR
+                , calculateInfo.buyCounts(), calculateInfo.buyProductsPrice(),
+                 calculateInfo.promotionPrice(), calculateInfo.memberShip(), calculateInfo.totalPrice()
+        );
+    }
+
+    public void showError(ErrorResponse errorResponse) {
+        System.out.println(errorResponse.message());
+    }
+}

리뷰: System.lineSeparator를 통한 줄바꿈 처리 굉장히 좋은 것 같습니다.
@@ -0,0 +1,46 @@
+package store.domain.service;
+
+import java.util.ArrayList;
+import java.util.List;
+import store.domain.BuyProducts;
+import store.domain.Product;
+import store.domain.Products;
+import store.view.dto.response.PromotionResponse;
+
+public class PromotionChecker {
+
+    private static final int ZERO = 0;
+
+    public List<PromotionResponse> checkPromotion(Products products, BuyProducts buyProducts) {
+        List<PromotionResponse> promotionResponses = new ArrayList<>();
+        for (Product buyProduct : buyProducts.products()) {
+            checkPromotionCase(products, buyProduct, promotionResponses);
+        }
+        return promotionResponses;
+    }
+
+    private void checkPromotionCase(Products products, Product buyProduct, List<PromotionResponse> promotionResponses) {
+        if (buyProduct.isPromotion()) {
+            Product promotionProduct = products.getPromotionProducts(buyProduct.getName()).getFirst();
+            checkNoPromotion(buyProduct, promotionProduct, promotionResponses);
+            checkFreePromotion(buyProduct, promotionResponses);
+        }
+    }
+
+    private void checkFreePromotion(Product buyProduct, List<PromotionResponse> promotionResponses) {
+        if (buyProduct.getQuantity() % buyProduct.getOnePromotionCycle() == buyProduct.getPromotionBuyQuantity()) {
+            promotionResponses.add(
+                    new PromotionResponse(buyProduct.getName(), false, ZERO, true, buyProduct.getPromotionGettableQuantity())
+            );
+        }
+    }
+
+    private void checkNoPromotion(Product buyProduct, Product promotionProduct, List<PromotionResponse> promotionResponses) {
+        if (buyProduct.getQuantity() > promotionProduct.getQuantity()) {
+            int noPromotionQuantity = buyProduct.getQuantity() - promotionProduct.getOnePromotionCycle() * promotionProduct.getPromotionQuantity();
+            promotionResponses.add(
+                    new PromotionResponse(buyProduct.getName(), true, noPromotionQuantity, false, ZERO)
+            );
+        }
+    }
+}

리뷰: 프로모션을 확인하는 부분을 따로 뽑은 이유가 있을까요?
@@ -0,0 +1,63 @@
+package store.domain;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import store.domain.exception.ProductErrorCode;
+import store.domain.exception.ProductException;
+
+public class BuyProductParser {
+
+    private static final int NAME_INDEX = 1;
+    private static final int PRICE_INDEX = 2;
+    private static final String INPUT_DELIMITER = ",";
+    private static final String ORDER_FORM = "\\[(.+)-(.+)]";
+
+    private final Map<String, String> products;
+
+    public BuyProductParser(String buyProduct) {
+        this.products = makeProductsForm(buyProduct);
+    }
+
+    public Map<String, String> getProducts() {
+        return Collections.unmodifiableMap(products);
+    }
+
+    private Map<String, String> makeProductsForm(String buyProducts) {
+        Map<String, String> products = new LinkedHashMap<>();
+        parseProducts(buyProducts, products);
+        return products;
+    }
+
+    private void parseProducts(String buyProducts, Map<String, String> products) {
+        Pattern pattern = Pattern.compile(ORDER_FORM);
+        parseByRest(buyProducts).forEach(product -> {
+            validateFrom(product);
+            Matcher matcher = pattern.matcher(product);
+            validateMatcher(matcher);
+            products.put(matcher.group(NAME_INDEX).trim(), matcher.group(PRICE_INDEX).trim());
+        });
+    }
+
+    private void validateMatcher(Matcher matcher) {
+        if (!matcher.matches()) {
+            throw new ProductException(ProductErrorCode.WRONG_INPUT);
+        }
+    }
+
+    private List<String> parseByRest(String buyProduct) {
+        if (buyProduct.contains(INPUT_DELIMITER)) {
+            return List.of(buyProduct.split(INPUT_DELIMITER));
+        }
+        return List.of(buyProduct);
+    }
+
+    private void validateFrom(String buyProducts) {
+        if (!buyProducts.matches(ORDER_FORM)) {
+            throw new ProductException(ProductErrorCode.WRONG_INPUT);
+        }
+    }
+}

리뷰: Hash의 경우 사기적인 시간복잡도로 좋지만 특수한 상황에서는 Hash의 경우 굉장히 느려질 수 있다고 생각합니다. 따라서 Hash보단 느리지만 안정적인 성능을 가지는 TreeMap도 추천드립니다. 근데 TreeMap을 사용하면 예시와 다르게 정렬된다는 단점이 있습니다.
@@ -1,7 +1,19 @@
 package store;
 
+import store.controller.StoreController;
+import store.domain.service.MemberShipCalculator;
+import store.domain.service.PromotionCalculator;
+import store.view.InputView;
+import store.view.OutputView;
+
 public class Application {
     public static void main(String[] args) {
         // TODO: 프로그램 구현
+        OutputView outputView = new OutputView();
+        InputView inputView = new InputView();
+        MemberShipCalculator memberShipCalculator = new MemberShipCalculator();
+        PromotionCalculator promotionCalculateService = new PromotionCalculator();
+        StoreController storeController = new StoreController(memberShipCalculator, promotionCalculateService, outputView, inputView);
+        storeController.run();
     }
 }

리뷰: controller 내부에서 생성자 주입으로 이미 명시를 해두었는데, 객체 생성을 한번 더 하신 이유가 궁금해요!
InputView와 OutputView는 static으로 호출하고 아래처럼 하시면 어떨까요??

```suggestion
        StoreController storeController = new StoreController(new MemberShipCalculator(), new PromotionCalculateService());
```
@@ -0,0 +1,79 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+import store.domain.vo.MemberShip;
+import store.domain.vo.ProductQuantity;
+import store.view.dto.response.BuyProductResponse;
+import store.view.dto.response.FreeProductResponse;
+import store.view.dto.response.ProductCalculateResponse;
+
+public class CustomerReceipt {
+
+    private final Receipt receipt;
+    private final MemberShip memberShipDiscount;
+
+    public CustomerReceipt(Receipt receipt, MemberShip memberShipDiscount) {
+        this.receipt = receipt;
+        this.memberShipDiscount = memberShipDiscount;
+    }
+
+    public List<BuyProductResponse> showBuyProducts() {
+        List<BuyProductResponse> products = new ArrayList<>();
+        for (Entry<Product, ProductQuantity> entry : receipt.getReceipt().entrySet()) {
+            products.add(new BuyProductResponse(entry.getKey().getName(), entry.getKey().getQuantity(),
+                    entry.getKey().getQuantity() * entry.getKey().getPrice()));
+        }
+        return Collections.unmodifiableList(products);
+    }
+
+    public List<FreeProductResponse> showFreeProductInfo() {
+        List<FreeProductResponse> freeProductResponses = new ArrayList<>();
+        for (Entry<Product, ProductQuantity> entry : receipt.getReceipt().entrySet()) {
+            freeProductResponses.add(
+                    new FreeProductResponse(entry.getKey().getName(), entry.getValue().getQuantity())
+            );
+        }
+        return Collections.unmodifiableList(freeProductResponses);
+    }
+
+    public ProductCalculateResponse showCalculateInfo() {
+        return new ProductCalculateResponse(
+                productsAllQuantity(),
+                productsAllPrice(),
+                promotionPrice(),
+                memberShipDiscount.getDisCountPrice(),
+                totalPrice()
+        );
+    }
+
+    private int totalPrice() {
+        return productsAllPrice() - promotionPrice() - memberShipDiscount.getDisCountPrice();
+    }
+
+    private int productsAllPrice() {
+        return receipt.getReceipt()
+                .keySet()
+                .stream()
+                .mapToInt(product -> product.getPrice() * product.getQuantity())
+                .sum();
+    }
+
+    private int promotionPrice() {
+        return receipt.getReceipt()
+                .entrySet()
+                .stream()
+                .mapToInt(entry -> entry.getKey().getPrice() * entry.getValue().getQuantity())
+                .sum();
+    }
+
+    private int productsAllQuantity() {
+        return receipt.getReceipt()
+                .keySet()
+                .stream()
+                .mapToInt(Product::getQuantity)
+                .sum();
+    }
+}

리뷰: 메서드명이 모두 직관적이어서 좋네요👍
@@ -0,0 +1,54 @@
+package store.domain;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import store.domain.exception.ProductErrorCode;
+import store.domain.exception.ProductException;
+
+public class FileReader {
+
+    private static final String CONTENT_SPLIT_DELIMITER = ",";
+
+    private final List<List<String>> fileContents;
+
+    public FileReader(String filePath) {
+        this.fileContents = makeFileContents(filePath);
+    }
+
+    public List<List<String>> fileContents() {
+        return Collections.unmodifiableList(fileContents);
+    }
+
+    private List<List<String>> makeFileContents(String filePath) {
+        List<List<String>> fileContents = new ArrayList<>();
+
+        try (BufferedReader fileReader = new BufferedReader(new java.io.FileReader(filePath))) {
+            skipOneLine(fileReader);
+            makeFileContent(fileReader, fileContents);
+        } catch (IOException e) {
+            throw new ProductException(ProductErrorCode.NOT_FOUND_FILE_PATH);
+        }
+
+        return fileContents;
+    }
+
+    private void makeFileContent(BufferedReader fileReader, List<List<String>> fileContents) throws IOException {
+        String line;
+
+        while ((line = fileReader.readLine()) != null) {
+            List<String> contentInfo = splitProducts(line);
+            fileContents.add(contentInfo);
+        }
+    }
+
+    private void skipOneLine(BufferedReader fileReader) throws IOException {
+        fileReader.readLine();
+    }
+
+    private List<String> splitProducts(String productInformation) {
+        return List.of(productInformation.split(CONTENT_SPLIT_DELIMITER));
+    }
+}

리뷰: List<List<String>>으로 구현하신 이유가 궁금합니다!
@@ -0,0 +1,45 @@
+package store.domain;
+
+import store.domain.exception.ProductErrorCode;
+import store.domain.exception.ProductException;
+
+public enum ProductInfo {
+
+    COKE("콜라", "1000"),
+    CIDER("사이다", "1000"),
+    ORANGE_JUICE("오렌지주스", "1800"),
+    SPARKLING_WATER("탄산수", "1200"),
+    WATER("물", "500"),
+    VITAMIN_WATER("비타민워터", "1500"),
+    POTATO_CHIPS("감자칩", "1500"),
+    CHOCOLATE_BAR("초코바", "1200"),
+    ENERGY_BAR("에너지바", "2000"),
+    MEAL_BOX("정식도시락", "6400"),
+    CUP_NOODLES("컵라면", "1700");
+
+    private final String name;
+    private final String price;
+
+    ProductInfo(String name, String price) {
+        this.name = name;
+        this.price = price;
+    }
+
+    public static String findPriceByName(String name) {
+        for (ProductInfo product : ProductInfo.values()) {
+            if (product.getName().equals(name)) {
+                return product.getPrice();
+            }
+        }
+
+        throw new ProductException(ProductErrorCode.NOT_FOUND_PRODUCT_NAME);
+    }
+
+    private String getName() {
+        return name;
+    }
+
+    private String getPrice() {
+        return price;
+    }
+}

리뷰: 해당 부분은 파일 내에 있는 내용을 활용해서 구현해야하지 않나용?? 따로 명시해두신 이유가 궁금합니다!