ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,24 @@
+package store.file;
+
+import store.product.Product;
+import store.promotion.Promotion;
+import store.promotion.PromotionManager;
+
+import java.io.InputStream;
+import java.util.List;
+
+public class FileReader {
+    public List<Product> createProduct() {
+        ProductParser productParser = new ProductParser(new PromotionManager(createPromotion()));
+        return productParser.read(road("products"));
+    }
+
+    public List<Promotion> createPromotion() {
+        PromotionParser promotionParser = new PromotionParser();
+        return promotionParser.read(road("promotions"));
+    }
+
+    public InputStream road(String filename) {
+        return getClass().getClassLoader().getResourceAsStream(filename + ".md");
+    }
+}

리뷰: 찾아보니 파일을 읽어오는 방법이더라고요. 처음봐서 신기했어요
@@ -0,0 +1,43 @@
+package store.file;
+
+import store.product.Product;
+import store.promotion.Promotion;
+import store.promotion.PromotionManager;
+import store.type.ProductType;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ProductParser {
+    private final PromotionManager promotionManager;
+
+    public ProductParser(PromotionManager promotionManager) {
+        this.promotionManager = promotionManager;
+    }
+
+    public List<Product> read(InputStream productsInputStream) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(productsInputStream))) {
+            return reader.lines()
+                    .skip(1)
+                    .map(this::parse)
+                    .collect(Collectors.toList());
+        } catch (IOException e) {
+            return Collections.emptyList();
+        }
+    }
+
+    public Product parse(String product) {
+        String[] productParts = product.split(",");
+        String name = productParts[ProductType.NAME.getValue()];
+        int price = Integer.parseInt(productParts[ProductType.PRICE.getValue()]);
+        int quantity = Integer.parseInt(productParts[ProductType.QUANTITY.getValue()]);
+        Promotion promotion = promotionManager.match(productParts[ProductType.PROMOTION.getValue()]);
+
+        return new Product(name, price, quantity, promotion);
+    }
+}

리뷰: readLine을 사용해 읽을때보다 stream을 활용하면 skip(1)을 통해 더 깔끔한 코드가 되는게 정말 좋은 것 같습니다.
skip(1)에서 1을 titleLine과 같은 상수로 처리해버린다면 가독성이 더욱 좋아질 것 같습니다.
@@ -0,0 +1,43 @@
+package store.file;
+
+import store.product.Product;
+import store.promotion.Promotion;
+import store.promotion.PromotionManager;
+import store.type.ProductType;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ProductParser {
+    private final PromotionManager promotionManager;
+
+    public ProductParser(PromotionManager promotionManager) {
+        this.promotionManager = promotionManager;
+    }
+
+    public List<Product> read(InputStream productsInputStream) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(productsInputStream))) {
+            return reader.lines()
+                    .skip(1)
+                    .map(this::parse)
+                    .collect(Collectors.toList());
+        } catch (IOException e) {
+            return Collections.emptyList();
+        }
+    }
+
+    public Product parse(String product) {
+        String[] productParts = product.split(",");
+        String name = productParts[ProductType.NAME.getValue()];
+        int price = Integer.parseInt(productParts[ProductType.PRICE.getValue()]);
+        int quantity = Integer.parseInt(productParts[ProductType.QUANTITY.getValue()]);
+        Promotion promotion = promotionManager.match(productParts[ProductType.PROMOTION.getValue()]);
+
+        return new Product(name, price, quantity, promotion);
+    }
+}

리뷰: getValue보다 getIdx와 같은 메서드로 만들어진다면 더 좋을 것 같습니다
@@ -0,0 +1,43 @@
+package store.file;
+
+import store.product.Product;
+import store.promotion.Promotion;
+import store.promotion.PromotionManager;
+import store.type.ProductType;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ProductParser {
+    private final PromotionManager promotionManager;
+
+    public ProductParser(PromotionManager promotionManager) {
+        this.promotionManager = promotionManager;
+    }
+
+    public List<Product> read(InputStream productsInputStream) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(productsInputStream))) {
+            return reader.lines()
+                    .skip(1)
+                    .map(this::parse)
+                    .collect(Collectors.toList());
+        } catch (IOException e) {
+            return Collections.emptyList();
+        }
+    }
+
+    public Product parse(String product) {
+        String[] productParts = product.split(",");
+        String name = productParts[ProductType.NAME.getValue()];
+        int price = Integer.parseInt(productParts[ProductType.PRICE.getValue()]);
+        int quantity = Integer.parseInt(productParts[ProductType.QUANTITY.getValue()]);
+        Promotion promotion = promotionManager.match(productParts[ProductType.PROMOTION.getValue()]);
+
+        return new Product(name, price, quantity, promotion);
+    }
+}

리뷰: 이것도 상수로 만든다면 더 좋을 것 같아요
@@ -0,0 +1,47 @@
+package store.file;
+
+import store.promotion.Promotion;
+import store.type.PromotionType;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class PromotionParser {
+    public List<Promotion> read(InputStream productsInputStream) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(productsInputStream))) {
+            return reader.lines()
+                    .skip(1)
+                    .map(this::parse)
+                    .collect(Collectors.toList());
+        } catch (IOException e) {
+            return Collections.emptyList();
+        }
+    }
+
+    public Promotion parse(String promotion) {
+        String[] promotionParts = promotion.split(",");
+        String name = promotionParts[PromotionType.NAME.getValue()];
+        int get = Integer.parseInt(promotionParts[PromotionType.GET.getValue()]);
+        int buy = Integer.parseInt(promotionParts[PromotionType.BUY.getValue()]);
+        LocalDate startDate = formatDate(promotionParts[PromotionType.START_DATE.getValue()]);
+        LocalDate endDate = formatDate(promotionParts[PromotionType.END_DATE.getValue()]);
+
+        return new Promotion(name, get, buy, startDate, endDate);
+    }
+
+    public LocalDate formatDate(String date) {
+        try {
+            DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+            return LocalDate.parse(date, dateFormat);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}

리뷰: 예외처리로 단순히 메세지를 보여주고 종료하는 것이 아닌 빈 리스트를 만들어 넘기는 부분에서 좋은 것 같습니다. 하지만 파일이 비어서 빈 리스트를 나타낸 것인지 파일을 읽다가 오류가 나서 빈 리스트인지에 대한 구분이 필요하다 생각해 추가적인 메세지를 띄우는 것이나 차이점을 만든다면 더 좋을 것 같습니다.
@@ -0,0 +1,47 @@
+package store.file;
+
+import store.promotion.Promotion;
+import store.type.PromotionType;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class PromotionParser {
+    public List<Promotion> read(InputStream productsInputStream) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(productsInputStream))) {
+            return reader.lines()
+                    .skip(1)
+                    .map(this::parse)
+                    .collect(Collectors.toList());
+        } catch (IOException e) {
+            return Collections.emptyList();
+        }
+    }
+
+    public Promotion parse(String promotion) {
+        String[] promotionParts = promotion.split(",");
+        String name = promotionParts[PromotionType.NAME.getValue()];
+        int get = Integer.parseInt(promotionParts[PromotionType.GET.getValue()]);
+        int buy = Integer.parseInt(promotionParts[PromotionType.BUY.getValue()]);
+        LocalDate startDate = formatDate(promotionParts[PromotionType.START_DATE.getValue()]);
+        LocalDate endDate = formatDate(promotionParts[PromotionType.END_DATE.getValue()]);
+
+        return new Promotion(name, get, buy, startDate, endDate);
+    }
+
+    public LocalDate formatDate(String date) {
+        try {
+            DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+            return LocalDate.parse(date, dateFormat);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}

리뷰: 단순히 null을 내보낸다면 에러를 잡아내기 어려울 수 있다고 생각합니다 추가적인 예외처리가 필요하다고 생각합니다.
@@ -0,0 +1,45 @@
+package store.io;
+
+import camp.nextstep.edu.missionutils.Console;
+import store.type.ErrorMessage;
+
+public class InputView {
+    private final OutputView outputView = new OutputView();
+
+    public String readAnswer() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                answerValidate(input);
+                return input;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public String readPurchaseProduct() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                purchaseProductValidate(input);
+                return input;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public void answerValidate(String input) {
+        if (!"Y".equals(input) && !"N".equals(input)) {
+            throw new IllegalArgumentException(ErrorMessage.INVALID_INPUT.getMessage());
+        }
+    }
+
+    public void purchaseProductValidate(String input) {
+        String regex = "\\[([a-zA-Z가-힣]+-[1-9][0-9]*)\\](,\\[([a-zA-Z가-힣]+-[1-9][0-9]*)\\])*";
+        if (!input.matches(regex)) {
+            throw new IllegalArgumentException(ErrorMessage.INVALID_FORMAT.getMessage());
+        }
+    }
+}

리뷰: 메서드에서 매번 생성되는 것보다는 상수로 올려두는 것은 어떤가요? 필드에 모여있으면 찾기도 편해서 더 좋을 것 같습니다.
@@ -0,0 +1,45 @@
+package store.io;
+
+import camp.nextstep.edu.missionutils.Console;
+import store.type.ErrorMessage;
+
+public class InputView {
+    private final OutputView outputView = new OutputView();
+
+    public String readAnswer() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                answerValidate(input);
+                return input;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public String readPurchaseProduct() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                purchaseProductValidate(input);
+                return input;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public void answerValidate(String input) {
+        if (!"Y".equals(input) && !"N".equals(input)) {
+            throw new IllegalArgumentException(ErrorMessage.INVALID_INPUT.getMessage());
+        }
+    }
+
+    public void purchaseProductValidate(String input) {
+        String regex = "\\[([a-zA-Z가-힣]+-[1-9][0-9]*)\\](,\\[([a-zA-Z가-힣]+-[1-9][0-9]*)\\])*";
+        if (!input.matches(regex)) {
+            throw new IllegalArgumentException(ErrorMessage.INVALID_FORMAT.getMessage());
+        }
+    }
+}

리뷰: Y와 N을 상수로 올리시면 좋을 것 같습니다. 프로그래밍 요구사항에 상수화가 있어서 참고하시면 좋을 것 같습니다
@@ -0,0 +1,52 @@
+package store.io;
+
+import store.object.Receipt;
+import store.product.Product;
+
+import java.util.List;
+
+public class OutputView {
+    public void greeting() {
+        System.out.println("안녕하세요. W편의점입니다.");
+        System.out.println("현재 보유하고 있는 상품입니다.\n");
+    }
+
+    public void printProducts(List<Product> products) {
+        products.forEach(System.out::println);
+    }
+
+    public void purchaseGuide() {
+        System.out.println("\n구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])");
+    }
+
+    public void promotionAdditionalGuide(String productName) {
+        System.out.printf("\n현재 %s은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)\n", productName);
+    }
+
+    public void promotionImpossibleGuide(String productName, int amount) {
+        System.out.printf("\n현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)\n", productName, amount);
+    }
+
+    public void membershipGuide() {
+        System.out.println("\n멤버십 할인을 받으시겠습니다? (Y/N)");
+    }
+
+    public void printReceipt(Receipt receipt) {
+        System.out.println("\n==============W 편의점================");
+        String productReportFormat = "%-19s%-10s%-6s";
+        System.out.printf((productReportFormat) + "%n", "상품명", "수량", "금액");
+        System.out.printf(receipt.productReport());
+        System.out.println("==============증    정================");
+        System.out.printf(receipt.promotionReport());
+        System.out.println("======================================");
+        System.out.printf(receipt.paymentReport());
+    }
+
+    public void closingGuide() {
+        System.out.println("\n감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)");
+    }
+
+    public void printError(String errorMessage) {
+        System.out.println("\n" + errorMessage);
+    }
+}

리뷰: toString을 활용한 깔끔한 처리가 좋은 것 같아요. 한가지 아쉽다면 StringBuilder와 같은 클래스를 사용해 출력할 메세지들을 모아서 한번에 출력하는데 IO작업이 줄어들어 더욱 좋을 것 같습니다.
@@ -0,0 +1,82 @@
+package store.object;
+
+import store.io.InputParser;
+import store.io.InputView;
+import store.io.OutputView;
+import store.product.ProductManager;
+
+import java.util.Map;
+
+public class ConvenienceStore {
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+    private final InputParser inputParser = new InputParser();
+    private final ProductManager productManager = new ProductManager();
+    private Receipt receipt;
+    private boolean isOpen = true;
+
+    public void open() {
+        while (isOpen) {
+            init();
+            announcement();
+            purchase();
+            close();
+            System.out.println();
+        }
+    }
+
+    public void init() {
+        receipt = new Receipt();
+    }
+
+    public void announcement() {
+        outputView.greeting();
+        productManager.print();
+    }
+
+    public void purchase() {
+        outputView.purchaseGuide();
+        while (true) {
+            try {
+                inputHandle();
+                return;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public void inputHandle() {
+        String purchaseProducts = inputView.readPurchaseProduct();
+        Map<String, Integer> purchaseData = inputParser.mapping(purchaseProducts);
+        productManager.validate(purchaseData);
+        createReceipt(purchaseData);
+    }
+
+    public void createReceipt(Map<String, Integer> purchaseData) {
+        purchaseData.forEach((name, amount) -> {
+            Amount purchaseAmount = productManager.purchase(name, amount);
+            receipt.addAmount(purchaseAmount);
+        });
+        if (receipt.isExistence()) {
+            membership();
+            showReceipt();
+        }
+    }
+
+    public void membership() {
+        outputView.membershipGuide();
+        receipt.membershipApply(inputView.readAnswer().trim());
+    }
+
+    public void showReceipt() {
+        outputView.printReceipt(receipt);
+    }
+
+    public void close() {
+        outputView.closingGuide();
+        if (inputView.readAnswer().equals("N")) {
+            isOpen = false;
+        }
+    }
+}

리뷰: 얘도 출력을 담당하는 부분이라 OutputView에 넣어 사용하면 더 좋을 것 같습니다.
@@ -0,0 +1,82 @@
+package store.object;
+
+import store.io.InputParser;
+import store.io.InputView;
+import store.io.OutputView;
+import store.product.ProductManager;
+
+import java.util.Map;
+
+public class ConvenienceStore {
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+    private final InputParser inputParser = new InputParser();
+    private final ProductManager productManager = new ProductManager();
+    private Receipt receipt;
+    private boolean isOpen = true;
+
+    public void open() {
+        while (isOpen) {
+            init();
+            announcement();
+            purchase();
+            close();
+            System.out.println();
+        }
+    }
+
+    public void init() {
+        receipt = new Receipt();
+    }
+
+    public void announcement() {
+        outputView.greeting();
+        productManager.print();
+    }
+
+    public void purchase() {
+        outputView.purchaseGuide();
+        while (true) {
+            try {
+                inputHandle();
+                return;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public void inputHandle() {
+        String purchaseProducts = inputView.readPurchaseProduct();
+        Map<String, Integer> purchaseData = inputParser.mapping(purchaseProducts);
+        productManager.validate(purchaseData);
+        createReceipt(purchaseData);
+    }
+
+    public void createReceipt(Map<String, Integer> purchaseData) {
+        purchaseData.forEach((name, amount) -> {
+            Amount purchaseAmount = productManager.purchase(name, amount);
+            receipt.addAmount(purchaseAmount);
+        });
+        if (receipt.isExistence()) {
+            membership();
+            showReceipt();
+        }
+    }
+
+    public void membership() {
+        outputView.membershipGuide();
+        receipt.membershipApply(inputView.readAnswer().trim());
+    }
+
+    public void showReceipt() {
+        outputView.printReceipt(receipt);
+    }
+
+    public void close() {
+        outputView.closingGuide();
+        if (inputView.readAnswer().equals("N")) {
+            isOpen = false;
+        }
+    }
+}

리뷰: 입력 파라미터에 넣지 않기위해 필드에 넣으신 느낌인데 지역 변수로 사용하는게 더 좋을 것 같습니다.
@@ -0,0 +1,82 @@
+package store.object;
+
+import store.io.InputParser;
+import store.io.InputView;
+import store.io.OutputView;
+import store.product.ProductManager;
+
+import java.util.Map;
+
+public class ConvenienceStore {
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+    private final InputParser inputParser = new InputParser();
+    private final ProductManager productManager = new ProductManager();
+    private Receipt receipt;
+    private boolean isOpen = true;
+
+    public void open() {
+        while (isOpen) {
+            init();
+            announcement();
+            purchase();
+            close();
+            System.out.println();
+        }
+    }
+
+    public void init() {
+        receipt = new Receipt();
+    }
+
+    public void announcement() {
+        outputView.greeting();
+        productManager.print();
+    }
+
+    public void purchase() {
+        outputView.purchaseGuide();
+        while (true) {
+            try {
+                inputHandle();
+                return;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public void inputHandle() {
+        String purchaseProducts = inputView.readPurchaseProduct();
+        Map<String, Integer> purchaseData = inputParser.mapping(purchaseProducts);
+        productManager.validate(purchaseData);
+        createReceipt(purchaseData);
+    }
+
+    public void createReceipt(Map<String, Integer> purchaseData) {
+        purchaseData.forEach((name, amount) -> {
+            Amount purchaseAmount = productManager.purchase(name, amount);
+            receipt.addAmount(purchaseAmount);
+        });
+        if (receipt.isExistence()) {
+            membership();
+            showReceipt();
+        }
+    }
+
+    public void membership() {
+        outputView.membershipGuide();
+        receipt.membershipApply(inputView.readAnswer().trim());
+    }
+
+    public void showReceipt() {
+        outputView.printReceipt(receipt);
+    }
+
+    public void close() {
+        outputView.closingGuide();
+        if (inputView.readAnswer().equals("N")) {
+            isOpen = false;
+        }
+    }
+}

리뷰: 얘를 boolean을 반환하게 해서 처리했다면 isOpen은 필드로 안나가도 되지 않았을까요?
@@ -0,0 +1,69 @@
+package store.product;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import store.object.Amount;
+import store.promotion.Promotion;
+import store.type.ErrorMessage;
+
+import java.util.Optional;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int quantity;
+    private final 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 int getQuantity() {
+        return quantity;
+    }
+
+    public Amount buy(int amount) {
+        if (promotion != null) {
+            return promotionBuy(amount);
+        }
+        isExceed(amount);
+        quantity -= amount;
+        return new Amount(name, price, amount, 0, 0, amount);
+    }
+
+    public Amount promotionBuy(int amount) {
+        if (promotion.isPossible(DateTimes.now())) {
+            Amount purchaseAmount = promotion.apply(name, price, quantity, amount);
+            quantity -= purchaseAmount.getBuy();
+            return purchaseAmount;
+        }
+        return new Amount(name, price, 0, 0, amount, 0);
+    }
+
+    public boolean isCorrect(String name) {
+        return name.equals(this.name);
+    }
+
+    public void isExceed(int amount) {
+        if (amount > quantity) {
+            throw new IllegalArgumentException(ErrorMessage.EXCEED_QUANTITY.getMessage());
+        }
+    }
+
+    public boolean isExist(String otherName) {
+        return this.name.equals(otherName);
+    }
+
+    @Override
+    public String toString() {
+        String promotionName = Optional.ofNullable(promotion)
+                .map(Promotion::toString)
+                .orElse("");
+        if (quantity == 0) {
+            return String.format("- %s %,d원 재고 없음 %s", name, price, promotionName).trim();
+        }
+        return String.format("- %s %,d원 %d개 %s", name, price, quantity, promotionName).trim();
+    }
+}

리뷰: 코드에 입력 파라미터로 0과 같은 특정한 값이 들어가는 경우 가독성을 매우 해친다고 생각합니다. 이보다는 Amount의 생성자를 0, 0을 넣지 않으면 자동으로 0이 들어가도록 작성해보는 것은 어떨까요?
@@ -0,0 +1,19 @@
+package store.type;
+
+public enum PromotionType {
+    NAME(0),
+    GET(1),
+    BUY(2),
+    START_DATE(3),
+    END_DATE(4);
+
+    private final int value;
+
+    PromotionType(int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+}

리뷰: 저도 이 부분은 그냥 필드로 작성하셨어도 괜찮다고 생각합니다