ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,71 @@
+package store.model.receipt;
+
+import store.global.MembershipDiscount;
+import store.model.Product;
+
+import java.text.DecimalFormat;
+
+public class AmountInfo {
+    private int totalPurchaseAmount;
+    private int totalPurchaseCount;
+    private int promotionDiscount;
+    private int membershipDiscount;
+
+    public AmountInfo() {
+        init();
+    }
+
+    private void init() {
+        totalPurchaseAmount = 0;
+        totalPurchaseCount = 0;
+        promotionDiscount = 0;
+        membershipDiscount = 0;
+    }
+
+    public void increaseTotal(int quantity, int price) {
+        totalPurchaseCount += quantity;
+        totalPurchaseAmount += quantity * price;
+    }
+
+    public void calculatePromotionDiscount(Product product, int giftCount) {
+        promotionDiscount += product.getPrice() * giftCount;
+    }
+
+    public void calculateMembershipDiscount(boolean isMembershipDiscount) {
+        if (!isMembershipDiscount) {
+            membershipDiscount = 0;
+            return;
+        }
+
+        DecimalFormat df = new DecimalFormat("#");
+        membershipDiscount = Integer.parseInt(df.format(membershipDiscount * MembershipDiscount.DISCOUNT_RATE.getValue()));
+
+        membershipDiscount = Math.min(membershipDiscount, (int)MembershipDiscount.MAX_DISCOUNT.getValue());
+    }
+
+    public void increaseMembershipDiscount(Product product, int giftCount, boolean getBenefit) {
+        if (giftCount == 0 & getBenefit) {
+            membershipDiscount += product.getPrice() * product.getQuantity();
+        }
+    }
+
+    public int getPayment() {
+        return totalPurchaseAmount - (promotionDiscount + membershipDiscount);
+    }
+
+    public int getTotalPurchaseAmount() {
+        return totalPurchaseAmount;
+    }
+
+    public int getTotalPurchaseCount() {
+        return totalPurchaseCount;
+    }
+
+    public int getPromotionDiscount() {
+        return promotionDiscount;
+    }
+
+    public int getMembershipDiscount() {
+        return membershipDiscount;
+    }
+}

리뷰: ``membershipDiscount``에 대한 계산 로직을 ``MembershipDiscount`` Enum 내부로 옮기는 방법을 고려해보시는 것은 어떨까요?
@@ -0,0 +1,75 @@
+package store.view;
+
+import store.model.GiftProduct;
+import store.model.Product;
+import store.model.receipt.AmountInfo;
+import store.model.receipt.GiftProducts;
+import store.model.receipt.PurchaseProducts;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class OutputView {
+    public void printStoreMenu() throws IOException {
+        System.out.println("""
+                안녕하세요. W편의점입니다.
+                현재 보유하고 있는 상품입니다.
+                """);
+
+        BufferedReader reader = new BufferedReader(new FileReader("src/main/resources/products.md"));
+        String product;
+
+        reader.readLine();
+
+        while ((product = reader.readLine()) != null) {
+            String[] str = product.split(",");
+            System.out.printf("- %s %s원 %s %s", str[0], String.format("%,d", Integer.parseInt(str[1])), changeNoStock(str[2]), changeNull(str[3]));
+            System.out.println();
+        }
+        reader.close();
+    }
+
+    public void printPurchaseProduct(PurchaseProducts purchaseProducts) {
+        System.out.println("==============W 편의점================");
+        System.out.println("상품명\t\t\t\t수량\t\t금액");
+        for (Product product : purchaseProducts.getPurchaseProducts()) {
+            System.out.printf("%-4s\t\t\t\t%s\t\t%s\n", product.getName(), product.getQuantity(), String.format("%,d", product.getQuantity() * product.getPrice()));
+        }
+    }
+
+    public void printGiftProducts(GiftProducts giftProducts) {
+        System.out.println("=============증\t\t정===============");
+        for (GiftProduct giftProduct : giftProducts.getGiftProducts()) {
+            System.out.printf("%-4s\t\t\t\t%d", giftProduct.getName(), giftProduct.getQuantity());
+            System.out.println();
+        }
+    }
+
+    public void printAmountInfo(AmountInfo amountInfo) {
+        System.out.println("====================================");
+        System.out.printf("총구매액\t\t\t\t%s\t\t%s", amountInfo.getTotalPurchaseCount(), String.format("%,d", amountInfo.getTotalPurchaseAmount()));
+        System.out.println();
+        System.out.printf("행사할인\t\t\t\t\t\t%s", String.format("-%,d", amountInfo.getPromotionDiscount()));
+        System.out.println();
+        System.out.printf("멤버십할인\t\t\t\t\t\t%s", String.format("-%,d", amountInfo.getMembershipDiscount()));
+        System.out.println();
+        System.out.printf("내실돈\t\t\t\t\t\t%s", String.format("%,d", amountInfo.getPayment()));
+        System.out.println();
+    }
+
+    private String changeNull(String input) {
+        if (input.equals("null")) {
+            return "";
+        }
+        return input;
+    }
+
+    private String changeNoStock(String input) {
+        if (input.equals("0")) {
+            return "재고 없음";
+        }
+        input = String.format("%,d", Integer.parseInt(input));
+        return input + "개";
+    }
+}

리뷰: ```suggestion
        System.out.printf("행사할인\t\t\t\t\t\t-%,d", amountInfo.getPromotionDiscount());
```
이렇게 사용하면 ``System.out.printf()``와 ``String.format()``를 함께 사용하지 않고도 출력할 수 있지 않을까요?
@@ -0,0 +1,67 @@
+package store.service;
+
+import store.model.Product;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class StockManageService {
+    private final List<String> foods;
+
+    public StockManageService() {
+        foods = new ArrayList<>();
+    }
+
+    public void bringStock() throws IOException {
+        BufferedReader reader = new BufferedReader(new FileReader("src/main/resources/products.md"));
+        String line;
+        boolean noStock = true;
+
+        while ((line = reader.readLine()) != null) {
+            foods.add(line);
+            if (!line.split(",")[2].equals("0") & !line.split(",")[2].equals("quantity")) {
+                noStock = false;
+            }
+        }
+        reader.close();
+
+        if (noStock) {
+            throw new IllegalArgumentException("[ERROR] 재고가 없습니다.");
+        }
+    }
+
+    public void manageStock(int countPurchaseNormal, int countPurchasePromotion, Product product) {
+        String targetName = product.getName();
+
+        for (int i = 0; i < foods.size(); i++) {
+            String[] food = foods.get(i).split(",");
+            if (food[0].equals(targetName)) {
+                int newStock = updateStock(food, countPurchaseNormal, countPurchasePromotion);
+                String updateStock = food[0] + "," + food[1] + "," + newStock + "," + food[3];
+                foods.set(i, updateStock);
+            }
+        }
+    }
+
+    private int updateStock(String[] food, int countPurchaseNormal, int countPurchasePromotion) {
+        int currentSock = Integer.parseInt(food[2]);
+        if (food[3].equals("null")) {
+            return currentSock - countPurchaseNormal;
+        }
+        return currentSock - countPurchasePromotion;
+    }
+
+    public void reflectStock() throws IOException {
+        BufferedWriter writer = new BufferedWriter(new FileWriter("src/main/resources/products.md", false));
+
+        for (String updatedLine : foods) {
+            writer.write(updatedLine + "\r\n");
+        }
+        writer.close();
+    }
+}

리뷰: 조건문이 의미를 알기 어려운 거 같아요!
메서드로 분리해보시는 건 어떨까요?
@@ -0,0 +1,67 @@
+package store.service;
+
+import store.model.Product;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class StockManageService {
+    private final List<String> foods;
+
+    public StockManageService() {
+        foods = new ArrayList<>();
+    }
+
+    public void bringStock() throws IOException {
+        BufferedReader reader = new BufferedReader(new FileReader("src/main/resources/products.md"));
+        String line;
+        boolean noStock = true;
+
+        while ((line = reader.readLine()) != null) {
+            foods.add(line);
+            if (!line.split(",")[2].equals("0") & !line.split(",")[2].equals("quantity")) {
+                noStock = false;
+            }
+        }
+        reader.close();
+
+        if (noStock) {
+            throw new IllegalArgumentException("[ERROR] 재고가 없습니다.");
+        }
+    }
+
+    public void manageStock(int countPurchaseNormal, int countPurchasePromotion, Product product) {
+        String targetName = product.getName();
+
+        for (int i = 0; i < foods.size(); i++) {
+            String[] food = foods.get(i).split(",");
+            if (food[0].equals(targetName)) {
+                int newStock = updateStock(food, countPurchaseNormal, countPurchasePromotion);
+                String updateStock = food[0] + "," + food[1] + "," + newStock + "," + food[3];
+                foods.set(i, updateStock);
+            }
+        }
+    }
+
+    private int updateStock(String[] food, int countPurchaseNormal, int countPurchasePromotion) {
+        int currentSock = Integer.parseInt(food[2]);
+        if (food[3].equals("null")) {
+            return currentSock - countPurchaseNormal;
+        }
+        return currentSock - countPurchasePromotion;
+    }
+
+    public void reflectStock() throws IOException {
+        BufferedWriter writer = new BufferedWriter(new FileWriter("src/main/resources/products.md", false));
+
+        for (String updatedLine : foods) {
+            writer.write(updatedLine + "\r\n");
+        }
+        writer.close();
+    }
+}

리뷰: 남은 재고를 다시 파일에 작성하셨군요! 인상적입니다!
`CRLF(\r\n)`는 운영체제에 종속적인 거 같아요!
`System.lineSeparator()`를 사용해보시는 걸 추천드려요!
@@ -0,0 +1,39 @@
+package store.model.receipt;
+
+import store.model.Product;
+
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PurchaseProducts {
+    private final List<Product> purchaseProducts;
+
+    public PurchaseProducts(String input) throws IOException {
+        purchaseProducts = new ArrayList<>();
+        validate(input);
+    }
+
+    private void validate(String input) throws IOException {
+        String[] inputProducts = input.split(",", -1);
+        for (String product : inputProducts) {
+            Product pro = new Product(product);
+            validate(pro);
+            purchaseProducts.add(pro);
+        }
+    }
+
+    private void validate(Product product){
+        String target = product.getName();
+        for (Product pro : purchaseProducts) {
+            if (pro.getName().equals(target)) {
+                throw new IllegalArgumentException("[ERROR] 중복되는 상품 이름이 있습니다.");
+            }
+        }
+    }
+
+    public List<Product> getPurchaseProducts() {
+        return purchaseProducts;
+    }
+}

리뷰: 이중 for문으로 확인하면, 시간이 좀 걸릴 수 있을 거 같아요!
`Set`이나 `stream`을 활용해보시면 좋을 거 같아요!
@@ -0,0 +1,36 @@
+package store;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import store.model.Product;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class ProductTest {
+    @DisplayName("올바르지 않은 제품 형식을 입력하면 예외가 발생한다.")
+    @EmptySource //null 검사를 해야 하나?
+    @ParameterizedTest
+    @ValueSource(strings = {"[", "]", "[-", "-]", "[-]", "[감자칩-]", "[-3]",""," "})
+    void throwWhenInvalidProductForm(String input) {
+        assertThatThrownBy(() -> new Product(input))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @DisplayName("존재하지 않는 제품 이름을 입력하면 예외가 발생한다.")
+    @ParameterizedTest
+    @ValueSource(strings = {"[포르쉐-3]"})
+    void throwWhenInvalidProductName(String input) {
+        assertThatThrownBy(() -> new Product(input))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @DisplayName("올바르지 않은 제품 수량을 입력하면 예외가 발생한다.")
+    @ParameterizedTest
+    @ValueSource(strings = {"[감자칩-감]", "[감자칩-0]", "[감자칩-1000]"})
+    void throwWhenInvalidProductQuantity(String input) {
+        assertThatThrownBy(() -> new Product(input))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+}

리뷰: 꼼꼼한 테스트가 인상적입니다!
개인적으로, 예외 테스트는 예외 메시지까지 판별해야 더 정확하다고 생각합니다!
예외 메시지를 상수화하시면, 테스트에도 사용할 수 있어서 좋을 거 같아요!
@@ -0,0 +1,58 @@
+package store.view;
+
+import static store.global.InputConstant.BUY_PRODUCT_MESSAGE;
+import static store.global.InputConstant.ENDING_MESSAGE;
+import static store.global.InputConstant.MEMBERSHIP_MESSAGE;
+import static store.global.InputConstant.NOTICE_FOR_FREE_STOCK;
+import static store.global.InputConstant.NOTICE_FOR_FREE_STOCK_MESSAGE;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+public class InputView {
+
+    private InputView() {
+
+    }
+
+    public static List<String> getLines(BufferedReader br) throws IOException {
+        List<String> lines = new ArrayList<>();
+        String line;
+        while ((line = getLine(br)) != null) {
+            lines.add(line);
+        }
+        return lines;
+    }
+
+    public static String getLine(BufferedReader br) throws IOException {
+        return br.readLine();
+    }
+
+    public static String getBuyProductMessage(Scanner sc) {
+        System.out.println(BUY_PRODUCT_MESSAGE);
+        return sc.nextLine();
+    }
+
+    public static String getMembershipMessage(Scanner sc) {
+        System.out.println(MEMBERSHIP_MESSAGE);
+        return sc.nextLine();
+    }
+
+    public static String getEndingMessage(Scanner sc) {
+        System.out.println(ENDING_MESSAGE);
+        return sc.nextLine();
+    }
+
+    public static String getMoreFreeStock(Scanner sc, String name, int freeStock) {
+        System.out.printf(NOTICE_FOR_FREE_STOCK_MESSAGE, name, freeStock);
+        return sc.nextLine();
+    }
+
+    public static String getAgreeBuyWithNoPromotion(Scanner sc, String name, int stock) {
+        System.out.printf(NOTICE_FOR_FREE_STOCK, name, stock);
+        return sc.nextLine();
+    }
+}

리뷰: `사용자가 입력하는 값은 camp.nextstep.edu.missionutils.Console의 readLine()을 활용한다.`라는 프로그래밍 요구사항이 있어서 Scanner를 사용하면 안될 것 같습니다!
@@ -0,0 +1,38 @@
+package store.domain.promotion;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+public class Promotion {
+
+    private final String name;
+    private final Range range;
+    private final BuyGet buyGet;
+
+    public Promotion(String name, BuyGet buyGet, Range range) {
+        this.buyGet = buyGet;
+        this.name = name;
+        this.range = range;
+    }
+
+    public boolean checkDate(LocalDateTime now) {
+        LocalDate date = now.toLocalDate();
+        return range.isValidRange(date);
+    }
+
+    public int calculateFreeStock(int stock) {
+        return buyGet.calculateGetStock(stock);
+    }
+
+    public int getTotalBuyStock(int totalStock, int currentStock) {
+        return buyGet.calculateBuyStock(totalStock, currentStock);
+    }
+
+    public int getBuyStock() {
+        return buyGet.getBuyStock();
+    }
+
+    public String getName() {
+        return name;
+    }
+}

리뷰: BuyGet과 Range로 분리한 이유가 궁금합니다!
@@ -0,0 +1,70 @@
+package store.controller.product;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static store.domain.Store.addFreeProduct;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.util.List;
+import store.domain.Item;
+import store.domain.Stock;
+import store.domain.Store;
+
+public class PromotionItemController {
+
+    public Item processPromotionItem(Stock stock, Item promotionalItem, List<Item> purchasedItems, Store store) {
+        Stock stockFreeItem = new Stock(0);
+        Stock stockForPay = new Stock(0);
+        Item purchasedItem = null;
+        if (promotionalItem == null) {
+            return null;
+        }
+        if (promotionalItem.checkDate(DateTimes.now())) {
+            Item item = store.buyPromoItemNoDiscount(stock, promotionalItem, purchasedItems);
+            if (item != null) {
+                return item;
+            }
+            purchasedItem = processPromotionItems(stock, promotionalItem, purchasedItems, stockFreeItem, stockForPay);
+        }
+
+        return purchasedItem;
+    }
+
+    public static int processRemainingPromotionStock(Item promotionalItem, Stock remainStock) {
+        return Math.min(promotionalItem.getStockCount(), remainStock.getStock());
+    }
+
+    private Item processPromotionItems(Stock stock, Item promotionalItem, List<Item> purchasedItems, Stock stockFreeItem,
+                                       Stock stockForPay) {
+        Item purchasedItem;
+        promotionProcess(stock, promotionalItem, stockFreeItem, stockForPay);
+        purchasedItem = new Item(promotionalItem, stockForPay, FALSE);
+        addFreeProduct(stockFreeItem, promotionalItem, purchasedItems, TRUE);
+        return purchasedItem;
+    }
+
+    private static void promotionProcess(Stock stock, Item promotionalItem, Stock stockFreeItem, Stock stockForPay) {
+        int stockForPromotionItem = promotionalItem.getTotalBuyStock(stock.getStock(), promotionalItem.getStockCount());
+        int freeStock = promotionalItem.calculateFreeStock(stockForPromotionItem);
+
+        updatePromotionItems(promotionalItem, stockFreeItem, stockForPay, freeStock, stockForPromotionItem);
+
+        stock.minus(stockForPromotionItem + freeStock);
+        int remainingStock = updateRemainItem(stock, promotionalItem, stockForPay);
+        stock.minus(remainingStock);
+    }
+
+    private static int updateRemainItem(Stock stock, Item promotionalItem, Stock stockForPay) {
+        int remainingStock = processRemainingPromotionStock(promotionalItem, stock);
+        promotionalItem.updateStock(remainingStock);
+        stockForPay.plus(remainingStock);
+        return remainingStock;
+    }
+
+    private static void updatePromotionItems(Item promotionalItem, Stock stockFreeItem, Stock stockForPay, int freeStock,
+                                             int stockForPromotionItem) {
+        stockFreeItem.plus(freeStock);
+        stockForPay.plus(stockForPromotionItem);
+        promotionalItem.updateStock(stockForPromotionItem + freeStock);
+    }
+}

리뷰: 프로그래밍 요구사항에 메서드 라인 제약이 있기 때문에 요구사항은 만족되어야 할 것 같습니다!
@@ -0,0 +1,110 @@
+package store.domain;
+
+import java.util.List;
+
+
+public class Receipt {
+
+    private final List<Item> items;
+    private int totalPrice;
+    private int promotionPrice;
+    private boolean membership;
+    private int membershipPrice;
+
+    public Receipt(List<Item> items, boolean membership) {
+        this.items = items;
+        this.membership = membership;
+    }
+
+    public Item findPromotionProduct(Item promotionItem) {
+        for (Item item : items) {
+            if (item.getName().equals(promotionItem.getName()) && (item.getFree())) {
+                return item;
+            }
+        }
+        Item newItem = new Item(promotionItem, new Stock(0), true);
+        items.add(newItem);
+        return newItem;
+    }
+
+    public void setMembership(boolean membership) {
+        this.membership = membership;
+    }
+
+    public Item findPromotionProductWithNull(Item promotionItem) {
+        for (Item item : items) {
+            if (item.getName().equals(promotionItem.getName()) && (item.getPromotion() != null)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    public void calculatePrice() {
+        for (Item item : items) {
+            totalPrice += item.calculatePrice();
+            if (item.getFree()) {
+                promotionPrice += item.calculatePrice();
+            }
+        }
+    }
+
+    public int calculateStock(Item target) {
+        int count = 0;
+        for (Item item : items) {
+            if (item.getName().equals(target.getName())) {
+                count += item.getStockCount();
+            }
+        }
+        return count;
+    }
+
+    public int totalStock() {
+        int total = 0;
+        for (Item item : items) {
+            total += item.getStockCount();
+        }
+        return total;
+    }
+
+    public boolean getMembership() {
+        return membership;
+    }
+
+    public List<Item> getProducts() {
+        return items;
+    }
+
+    public int getPromotionPrice() {
+        return promotionPrice;
+    }
+
+    public int getTotalPrice() {
+        return totalPrice;
+    }
+
+    public int getMembershipPrice() {
+        return membershipPrice;
+    }
+
+    public Integer applyMembership() {
+        System.out.println();
+        int discountPrice = 0;
+        for (Item item : items) {
+            if (item.getFree()) {
+                int buyStock = item.getBuyStockByFreeStock(item.getStockCount());
+                discountPrice += buyStock * item.getPrice();
+            }
+        }
+        if (getMembership()) {
+            return calculateMembershipPrice(discountPrice);
+        }
+        return 0;
+    }
+
+    public int calculateMembershipPrice(int discountPrice) {
+        membershipPrice = Math.min((int) ((totalPrice - promotionPrice - discountPrice) * 0.3), 8000);
+        return membershipPrice;
+    }
+
+}

리뷰: 이 메서드에서 개행처리를 하는 건 좋지 않은 것 같습니다! 그리고 메서드 라인 요구사항을 지켜야 합니다!
@@ -0,0 +1,62 @@
+package store.domain;
+
+import static store.global.ErrorMessages.INVALID_STATE_ERROR;
+
+import java.util.Objects;
+
+public class Stock {
+
+    private int stock;
+
+    public Stock(int stock) {
+        validate(stock);
+        this.stock = stock;
+    }
+
+    public int getStock() {
+        return stock;
+    }
+
+    public boolean compare(int stock) {
+        return this.stock >= stock;
+    }
+
+    public void minus(int stock) {
+        updateValidate(stock);
+        this.stock -= stock;
+    }
+
+    public void plus(int stock) {
+        this.stock += stock;
+    }
+
+    private void validate(int stock) {
+        if (stock < 0) {
+            throw new IllegalStateException(INVALID_STATE_ERROR.getMessage());
+        }
+    }
+
+    private void updateValidate(int stock) {
+        if (stock > this.stock) {
+            throw new IllegalStateException(INVALID_STATE_ERROR.getMessage());
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Stock stock1 = (Stock) o;
+        return stock == stock1.stock;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(stock);
+    }
+}
+

리뷰: 이 부분을 따로 Override하신 이유가 궁금해요!
@@ -0,0 +1,25 @@
+package store.view;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class FileInput {
+
+    private FileInput() {
+
+    }
+
+    public static final String ITEM_FILE_NAME = "products.md";
+    public static final String PROMOTION_FILE_NAME = "promotions.md";
+
+    public static BufferedReader FileInputSetting(String fileName) {
+        InputStream inputStream = FileInput.class.getClassLoader().getResourceAsStream(fileName);
+
+        if (inputStream == null) {
+            throw new IllegalStateException("File not found: products.md");
+        }
+
+        return new BufferedReader(new InputStreamReader(inputStream));
+    }
+}

리뷰: 이 부분 예외 메세지의 경우도 상수에서 가져와야 할 것 같습니다!