ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,84 @@
+package store.controller;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.io.IOException;
+import java.util.Map;
+import store.constants.ErrorMessage;
+import store.constants.StringConstants;
+import store.domain.Cart;
+import store.domain.Products;
+import store.domain.Promotions;
+import store.domain.Purchase;
+import store.domain.TotalPrice;
+import store.service.CartService;
+import store.service.InitService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class StoreController {
+
+    private Products products;
+    private Promotions promotions;
+    private Cart cart;
+
+    private final InitService initService = new InitService();
+    private final PromotionService promotionService = new PromotionService();
+    private final CartService cartService = new CartService();
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+
+    public void run() {
+        String next = StringConstants.YES.getString();
+        initStock();
+        while(next.equals(StringConstants.YES.getString())) {
+            start();
+            checkProducts();
+            result();
+            products = cartService.productsReduce(products, cart);
+            next = inputView.checkMorePurchase();
+        }
+        Console.close();
+    }
+
+    private void initStock() {
+        try {
+            products = initService.saveInitProducts();
+            promotions = initService.saveInitPromotions();
+        } catch (IOException e) {
+            System.out.println(ErrorMessage.SYSTEM_ERROR.getMessage());
+        }
+    }
+
+    private void start() {
+        outputView.productsOutput(this.products);
+        cart = inputView.readItem(products);
+    }
+
+    private void checkProducts() {
+        for (Purchase purchase : cart.getItems()) {
+            promotionService.setPromotion(purchase, products);
+            if (!purchase.getPromotion().equals(StringConstants.NO_PROMOTION.getString())) {
+                promotionService.promotionsAll(purchase, promotions, products, cart);
+            }
+        }
+        addCart();
+    }
+
+    private void addCart() {
+        promotionService.applyAddPurchaseToCart(cart);
+    }
+
+    private void result() {
+        Map<String, Purchase> items = cart.mergeItems(cart.getItems());
+        TotalPrice totalPrice = new TotalPrice(cart.getItems(), products);
+
+        totalPrice.setPromotionPrice(totalPrice.getPromotionSalePrice(cart.getItems(), promotions, products));
+        String answer = inputView.checkMemberShipSale();
+
+        if (answer.equals(StringConstants.YES.getString())) {
+            totalPrice.setMemberShipPrice(totalPrice.getMembershipSalePrice(cart.getItems(), products));
+        }
+        outputView.printReceipt(cart, products, promotions, items, totalPrice);
+    }
+}

리뷰: service 계층을 분리했다면 비즈니스 로직은 service에게 위임하는 것도 좋을 것 같아요!
@@ -0,0 +1,56 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import store.constants.ErrorMessage;
+
+public class Cart {
+
+    private List<Purchase> items;
+
+    public Cart(List<Purchase> items) {
+        this.items = new ArrayList<>();
+        addPurchase(new ArrayList<>(items));
+    }
+
+    public List<Purchase> getItems() {
+        return items;
+    }
+
+    private void addPurchase(List<Purchase> purchases) {
+        for (Purchase purchase : purchases) {
+            if(validateItem(purchase)) {
+                this.items.add(purchase);
+            }
+        }
+    }
+
+    private boolean validateItem(Purchase purchase) {
+        if (items.stream().anyMatch(item -> item.getName().equals(purchase.getName()))) {
+            throw new IllegalArgumentException(ErrorMessage.DUPLICATE_PRODUCT_INPUT_ERROR.getMessage());
+        }
+        return true;
+    }
+
+    public Map<String, Purchase> mergeItems(List<Purchase> purchases) {
+        Map<String, Purchase> itemsMap = new HashMap<>();
+        for (Purchase purchase : purchases) {
+            addItems(purchase, itemsMap);
+        }
+        return itemsMap;
+    }
+
+    private static void addItems(Purchase purchase, Map<String, Purchase> itemsMap) {
+        Purchase existingItem = itemsMap.get(purchase.getName());
+
+        if (existingItem != null) {
+            existingItem.setQuantity(existingItem.getQuantity() + purchase.getQuantity());
+        }
+        if (existingItem == null) {
+            Purchase item = new Purchase(purchase.getName(), purchase.getQuantity(), purchase.getPromotion());
+            itemsMap.put(item.getName(), item);
+        }
+    }
+}

리뷰: stream을 활용해볼 수 있지 않을까요..? 🤔
@@ -0,0 +1,56 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import store.constants.ErrorMessage;
+
+public class Cart {
+
+    private List<Purchase> items;
+
+    public Cart(List<Purchase> items) {
+        this.items = new ArrayList<>();
+        addPurchase(new ArrayList<>(items));
+    }
+
+    public List<Purchase> getItems() {
+        return items;
+    }
+
+    private void addPurchase(List<Purchase> purchases) {
+        for (Purchase purchase : purchases) {
+            if(validateItem(purchase)) {
+                this.items.add(purchase);
+            }
+        }
+    }
+
+    private boolean validateItem(Purchase purchase) {
+        if (items.stream().anyMatch(item -> item.getName().equals(purchase.getName()))) {
+            throw new IllegalArgumentException(ErrorMessage.DUPLICATE_PRODUCT_INPUT_ERROR.getMessage());
+        }
+        return true;
+    }
+
+    public Map<String, Purchase> mergeItems(List<Purchase> purchases) {
+        Map<String, Purchase> itemsMap = new HashMap<>();
+        for (Purchase purchase : purchases) {
+            addItems(purchase, itemsMap);
+        }
+        return itemsMap;
+    }
+
+    private static void addItems(Purchase purchase, Map<String, Purchase> itemsMap) {
+        Purchase existingItem = itemsMap.get(purchase.getName());
+
+        if (existingItem != null) {
+            existingItem.setQuantity(existingItem.getQuantity() + purchase.getQuantity());
+        }
+        if (existingItem == null) {
+            Purchase item = new Purchase(purchase.getName(), purchase.getQuantity(), purchase.getPromotion());
+            itemsMap.put(item.getName(), item);
+        }
+    }
+}

리뷰: 사이드이펙트를 지양해야 한다는 의견이시군요!! 저도 동의합니다!
@@ -0,0 +1,49 @@
+package store.service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import store.domain.Product;
+import store.domain.Products;
+import store.domain.Promotion;
+import store.domain.Promotions;
+import store.utils.FileRead;
+import store.utils.Split;
+
+public class InitService {
+
+    private static final String PRODUCTS_FILE_NAME = "src/main/resources/products.md";
+    private static final String PROMOTIONS_FILE_NAME = "src/main/resources/promotions.md";
+
+    public Products saveInitProducts() throws IOException {
+        List<String> fileContent = FileRead.readFile(PRODUCTS_FILE_NAME);
+        List<Product> productList = new ArrayList<>();
+        Set<String> productNames = new LinkedHashSet<>();
+
+        saveProducts(fileContent, productNames, productList);
+        Products products = new Products(productList);
+        products.setProductNames(new ArrayList<>(productNames));
+        products.addNoRegularProducts();
+        return products;
+    }
+
+    private static void saveProducts(List<String> fileContent, Set<String> productNames, List<Product> productList) {
+        for (String content : fileContent) {
+            List<String> split = Split.commaSpliter(content);
+            productNames.add(split.get(0));
+            productList.add(new Product(split.get(0), split.get(1), split.get(2), split.get(3)));
+        }
+    }
+
+    public Promotions saveInitPromotions() throws IOException {
+        List<String> fileContent = FileRead.readFile(PROMOTIONS_FILE_NAME);
+        List<Promotion> promotions = new ArrayList<>();
+        for (String content : fileContent) {
+            List<String> split = Split.commaSpliter(content);
+            promotions.add(new Promotion(split.get(0), split.get(1), split.get(2), split.get(3), split.get(4)));
+        }
+        return new Promotions(promotions);
+    }
+}

리뷰: 방어적 복사를 하셨네요!
여기서 방어적 복사를 진행하지 않으면 발생할 수 있는 문제는 뭐가 있을까요!?
@@ -0,0 +1,49 @@
+package store.service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import store.domain.Product;
+import store.domain.Products;
+import store.domain.Promotion;
+import store.domain.Promotions;
+import store.utils.FileRead;
+import store.utils.Split;
+
+public class InitService {
+
+    private static final String PRODUCTS_FILE_NAME = "src/main/resources/products.md";
+    private static final String PROMOTIONS_FILE_NAME = "src/main/resources/promotions.md";
+
+    public Products saveInitProducts() throws IOException {
+        List<String> fileContent = FileRead.readFile(PRODUCTS_FILE_NAME);
+        List<Product> productList = new ArrayList<>();
+        Set<String> productNames = new LinkedHashSet<>();
+
+        saveProducts(fileContent, productNames, productList);
+        Products products = new Products(productList);
+        products.setProductNames(new ArrayList<>(productNames));
+        products.addNoRegularProducts();
+        return products;
+    }
+
+    private static void saveProducts(List<String> fileContent, Set<String> productNames, List<Product> productList) {
+        for (String content : fileContent) {
+            List<String> split = Split.commaSpliter(content);
+            productNames.add(split.get(0));
+            productList.add(new Product(split.get(0), split.get(1), split.get(2), split.get(3)));
+        }
+    }
+
+    public Promotions saveInitPromotions() throws IOException {
+        List<String> fileContent = FileRead.readFile(PROMOTIONS_FILE_NAME);
+        List<Promotion> promotions = new ArrayList<>();
+        for (String content : fileContent) {
+            List<String> split = Split.commaSpliter(content);
+            promotions.add(new Promotion(split.get(0), split.get(1), split.get(2), split.get(3), split.get(4)));
+        }
+        return new Promotions(promotions);
+    }
+}

리뷰: split의 각 요소가 무엇을 의미하는지 명확하지 않은 것 같아요. 내부 클래스 매핑 등을 통해 각 자리에 어떤 의미의 필드가 들어있는지 명시하는 것도 좋을 것 같습니다!
@@ -0,0 +1,21 @@
+package store.utils;
+
+import java.text.DecimalFormat;
+
+public class ConvertFormater {
+
+    private static final String COUNT_UNIT = "개";
+    private static final String ZERO_QUANTITY = "재고 없음";
+
+    public static String moneyFormat(int money) {
+        DecimalFormat df = new DecimalFormat("###,###");
+        return df.format(money);
+    }
+
+    public static String quantityFormat(int quantity) {
+        if (quantity == 0) {
+            return ZERO_QUANTITY;
+        }
+        return quantity + COUNT_UNIT;
+    }
+}

리뷰: 이 포맷은 상수화하지 않은 이유가 있으신가요!?
@@ -0,0 +1,21 @@
+package store.utils;
+
+import java.text.DecimalFormat;
+
+public class ConvertFormater {
+
+    private static final String COUNT_UNIT = "개";
+    private static final String ZERO_QUANTITY = "재고 없음";
+
+    public static String moneyFormat(int money) {
+        DecimalFormat df = new DecimalFormat("###,###");
+        return df.format(money);
+    }
+
+    public static String quantityFormat(int quantity) {
+        if (quantity == 0) {
+            return ZERO_QUANTITY;
+        }
+        return quantity + COUNT_UNIT;
+    }
+}

리뷰: 개인적으로 상수 네이밍 단어를 통일하는 것도 좋을 것 같아요! 
ex) `COUNT_UNIT` -> `QUANTITY_UNIT`
@@ -0,0 +1,14 @@
+package store.utils;
+
+import store.constants.ErrorMessage;
+
+public class Parser {
+
+    public static int parseNumberToInt(String number) {
+        try {
+            return Integer.parseInt(number);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(ErrorMessage.ETC_INPUT_ERROR.getMessage());
+        }
+    }
+}

리뷰: 예외를 재포장하는 로직이 좋은 것 같아요! 👍
@@ -1,7 +1,16 @@
 package store;
 
+import store.controller.Screen;
+import store.view.InputView;
+import store.view.OutputView;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        try {
+            Screen screen = new Screen(new InputView(), new OutputView());
+            screen.run();
+        } catch (IllegalArgumentException e) {
+            System.out.println("[ERROR] " + e.getMessage());
+        }
     }
 }

리뷰: 에러 메세지를 띄우고 해당 위치에서 다시 시작을 해야하는데 그 부분이 빠진 것 같습니다. 또한 에러를 출력하는 부분도 결국 view에서 해줘야한다고 생각합니다.
@@ -0,0 +1,16 @@
+package store.constant;
+
+public enum MembershipDiscount {
+    MAX_DISCOUNT(8000),
+    DISCOUNT_RATE(0.3);
+
+    private final double value;
+
+    MembershipDiscount(double value) {
+        this.value = value;
+    }
+
+    public double getValue() {
+        return value;
+    }
+}

리뷰: 8000도 double을 이용하기에는 별로 좋지않은 것 같습니다. 필드에 저장하거나 일반 클래스의 필드로 만드는 것은 어떨까요?
@@ -0,0 +1,22 @@
+package store.constant;
+
+public enum StockState {
+    NO_STOCK("0", "재고 없음"),
+    HAVE_STOCK("1", "");
+
+    private final String message;
+    private final String stockState;
+
+    StockState(String message, String stockState) {
+        this.message = message;
+        this.stockState = stockState;
+    }
+
+    public static String getMatchingState(String findAnswer) {
+        if (NO_STOCK.message.equals(findAnswer)) {
+            return NO_STOCK.stockState;
+        }
+        findAnswer = String.format("%,d", Integer.parseInt(findAnswer));
+        return findAnswer + "개";
+    }
+}

리뷰: enum의 성향보다는 그냥 재고 상태를 반환하는 느낌이 들어 비즈니스 로직에서 굳이 뺄 필요가 있냐는 생각이 들어요
@@ -0,0 +1,25 @@
+package store.constant;
+
+import store.exception.InvalidFormException;
+
+public enum YesNo {
+    YES("Y", true),
+    NO("N", false);
+
+    private final String message;
+    private final boolean answer;
+
+    YesNo(String message, Boolean answer) {
+        this.message = message;
+        this.answer = answer;
+    }
+
+    public static boolean getMatchingAnswer(String findAnswer) {
+        for (YesNo yesNo : YesNo.values()) {
+            if (yesNo.message.equals(findAnswer)) {
+                return yesNo.answer;
+            }
+        }
+        throw new InvalidFormException();
+    }
+}

리뷰: 이처럼 메서드 하나로 처리할 수 있는 것을 enum으로 처리하는 이유가 있을까요?
@@ -0,0 +1,7 @@
+package store.exception;
+
+public class InvalidDuplicateOrder extends IllegalArgumentException {
+    public InvalidDuplicateOrder() {
+        super("[ERROR] 중복되는 상품을 입력했습니다. 다시 입력해주세요.");
+    }
+}

리뷰: [ERROR]은 에러출력 형식이니 에러를 출력할때 합쳐도 좋을 것 같습니다.
또한 에러 메세지를 한곳에 모아두는게 더 좋을 것 같습니다. enum사용하시면 좋을 것 같습니다.
@@ -0,0 +1,7 @@
+package store.exception;
+
+public class InvalidDuplicateOrder extends IllegalArgumentException {
+    public InvalidDuplicateOrder() {
+        super("[ERROR] 중복되는 상품을 입력했습니다. 다시 입력해주세요.");
+    }
+}

리뷰: 하나의 커스텀 Exception을 만들지 않고 여러가지로 구현하신 이유가 있을까요?
@@ -0,0 +1,19 @@
+package store.model;
+
+public class GiftProduct {
+    private final String name;
+    private final int quantity;
+
+    public GiftProduct(Product promotion, int possibleGiftProducts) {
+        this.name = promotion.getName();
+        this.quantity = possibleGiftProducts / promotion.getPromotionCount();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+}

리뷰: 앞에서 추가적으로 연산하고 다시 원래대로 되돌리는 연산은 비효율적인 것 같습니다...
@@ -0,0 +1,19 @@
+package store.model;
+
+public class GiftProduct {
+    private final String name;
+    private final int quantity;
+
+    public GiftProduct(Product promotion, int possibleGiftProducts) {
+        this.name = promotion.getName();
+        this.quantity = possibleGiftProducts / promotion.getPromotionCount();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+}

리뷰: 만일 연산이 완료된 상태로 순수 데이터 저장을 위한 것이라면 record를 사용할 수 있으니 이 부분도 참고하시면 좋을 것 같습니다.
@@ -0,0 +1,65 @@
+package store.model;
+
+import store.util.Parser;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int quantity;
+    private final String promotion;
+    private final PromotionPolicy promotionPolicy;
+
+    public Product(String line) {
+        String[] product = line.split(",");
+        this.name = product[0];
+        this.price = Parser.parseToInt(product[1]);
+        this.quantity = Parser.parseToInt(product[2]);
+        this.promotion = product[3];
+        promotionPolicy = new PromotionPolicy(promotion);
+    }
+
+    public boolean isPromotionBenefitPossibleLeft(Order order) {
+        int promotionRemainingCount = getNoPromotionBenefit(order.getQuantity());
+        return promotionRemainingCount == getPromotionBuyCount();
+    }
+
+    public void reduceStock(int num) {
+        quantity -= num;
+    }
+
+    public boolean isPromotion() {
+        return !promotion.equals("null");
+    }
+
+    public boolean isPromotionPeriod() {
+        return promotionPolicy.isValidPeriod();
+    }
+
+    public int getNoPromotionBenefit(int num) {
+        return num % getPromotionCount();
+    }
+
+    public int getPromotionCount() {
+        return promotionPolicy.getPromotionCount();
+    }
+
+    public int getPromotionBuyCount() {
+        return promotionPolicy.getPromotionBuyCount();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+}

리뷰: 저도 이렇게 만드는 것을 생각해봤는데 이 부분이 View와 model의 경계에 있는 부분 같습니다. 저는 view에서 데이터를 정제하여 dto를 통해 전달하는 것도 나쁘지 않다고 생각하는데 hayeoungLee님은 어떻게 생각하시나요?
@@ -0,0 +1,65 @@
+package store.model;
+
+import store.util.Parser;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int quantity;
+    private final String promotion;
+    private final PromotionPolicy promotionPolicy;
+
+    public Product(String line) {
+        String[] product = line.split(",");
+        this.name = product[0];
+        this.price = Parser.parseToInt(product[1]);
+        this.quantity = Parser.parseToInt(product[2]);
+        this.promotion = product[3];
+        promotionPolicy = new PromotionPolicy(promotion);
+    }
+
+    public boolean isPromotionBenefitPossibleLeft(Order order) {
+        int promotionRemainingCount = getNoPromotionBenefit(order.getQuantity());
+        return promotionRemainingCount == getPromotionBuyCount();
+    }
+
+    public void reduceStock(int num) {
+        quantity -= num;
+    }
+
+    public boolean isPromotion() {
+        return !promotion.equals("null");
+    }
+
+    public boolean isPromotionPeriod() {
+        return promotionPolicy.isValidPeriod();
+    }
+
+    public int getNoPromotionBenefit(int num) {
+        return num % getPromotionCount();
+    }
+
+    public int getPromotionCount() {
+        return promotionPolicy.getPromotionCount();
+    }
+
+    public int getPromotionBuyCount() {
+        return promotionPolicy.getPromotionBuyCount();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+}

리뷰: 프로모션이 적용이 되었나라는 질문이니 프로모션 기간인지 확인하는 부분도 있으면 더 좋을 것 같습니다.
@@ -0,0 +1,86 @@
+package store.model;
+
+import java.util.List;
+
+public class Products {
+    public static final int HAVE_PROMOTION_BENEFIT_SIZE = 2;
+    public static final int HAVE_NO_PROMOTION_BENEFIT_SIZE = 1;
+    List<Product> products;
+
+    public Products(List<Product> products) {
+        this.products = products;
+    }
+
+    public boolean isGetOneFree(Order order) {
+        Product promotion = getPromotionProduct();
+        return isPromotionPeriod() &&
+                isPromotionMoreThanOrder(order, promotion) &&
+                isPromotionBenefitPossibleLeft(order, promotion) &&
+                isPromotionStockEnough(order, promotion);
+    }
+
+    public boolean isBuyOriginalPrice(Order order) {
+        return countBuyOriginalPrice(order) > 0;
+    }
+
+    public boolean isPromotionPeriod() {
+        Product promotion = getPromotionProduct();
+        return promotion.isPromotionPeriod();
+    }
+
+    private boolean isPromotionMoreThanOrder(Order order, Product promotion) {
+        return promotion.getQuantity() > order.getQuantity();
+    }
+
+    private boolean isPromotionBenefitPossibleLeft(Order order, Product promotion) {
+        return promotion.isPromotionBenefitPossibleLeft(order);
+    }
+
+    private boolean isPromotionStockEnough(Order order, Product promotion) {
+        return promotion.getQuantity() >= order.getQuantity() + 1;
+    }
+
+    public int countBuyOriginalPrice(Order order) {
+        Product promotion = getPromotionProduct();
+        if (promotion.isPromotionPeriod() && !isPromotionMoreThanOrder(order, promotion)) {
+            return promotion.getNoPromotionBenefit(promotion.getQuantity()) + order.getQuantity() - promotion.getQuantity();
+        }
+        return 0;
+    }
+
+    public int countReducePromotionWhen(Order order) {
+        return order.getQuantity() - countBuyOriginalPrice(order);
+    }
+
+    public void doNotOrderOriginalPrice(Order order) {
+        order.decreaseQuantity(countBuyOriginalPrice(order));
+    }
+
+    public boolean isOrderQuantityBuyOnlyPromotionStock(Order order) {
+        return countBuyOriginalPrice(order) == order.getQuantity();
+    }
+
+    public Product getPromotionProduct() {
+        if (products.size() == HAVE_PROMOTION_BENEFIT_SIZE) {
+            return products.get(0);
+        }
+        if (products.size() == HAVE_NO_PROMOTION_BENEFIT_SIZE) {
+            return new Product(getNormalProduct().getName() + "," + getNormalProduct().getPrice() + ",0,null");
+        }
+        throw new IllegalArgumentException();
+    }
+
+    public Product getNormalProduct() {
+        if (products.size() == HAVE_PROMOTION_BENEFIT_SIZE) {
+            return products.get(1);
+        }
+        if (products.size() == HAVE_NO_PROMOTION_BENEFIT_SIZE) {
+            return products.get(0);
+        }
+        throw new IllegalArgumentException();
+    }
+
+    public List<Product> getProducts() {
+        return products;
+    }
+}

리뷰: ```suggestion
        return promotion.getQuantity() > order.getQuantity();
```
이렇게 바꾸시는 것이 더 나을것 같습니다.