ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,34 @@
+package store.error;
+
+import store.model.item.Item;
+import store.model.order.OrderItem;
+
+public class PromotionConfirmationForFreeException extends RuntimeException {
+    private Item item;
+    private OrderItem orderItem;
+    private int shortfall;
+
+    public PromotionConfirmationForFreeException(Item item, OrderItem orderItem, int shortfall) {
+        super();
+        this.item = item;
+        this.orderItem = orderItem;
+        this.shortfall = shortfall;
+    }
+
+
+    public Item getItem() {
+        return item;
+    }
+
+    public OrderItem getOrderItem() {
+        return orderItem;
+    }
+
+    public int getOrderQuantity() {
+        return orderItem.getQuantity();
+    }
+
+    public int getShortfall() {
+        return shortfall;
+    }
+}

리뷰: 제 구조에서는  view는 controller에서만 활동이가능합니다. (사용자 입력 출력 조정)
그렇기 때문에 비즈니스 로직에서 view를 바로 사용한다면 해당 예외가 필요없겠지만 흐름을 controller로 가져오기위해서는 예외밖에 생각이 나지 않았습니다. 그렇기에 view를 써야하는 상황에서 예외를 발생시켜서 흐름을 controller로 가져오고난 controller가 view를 동작시키도록 구현햇습니다. 

선택을해야했습니다. 비즈니스 로직에서 view를 사용하느냐? 아님 흐름을 controller로 가져와서 controller가 상황에 맞게 view를 동작하게 하느냐
@@ -0,0 +1,23 @@
+package store.model;
+
+import java.util.function.Function;
+
+public enum SalesAmountType {
+    MEMBERSHIP("멤버십할인", Membership::calculateDiscount);
+
+    private final String viewName;
+    private final Function<Long, Long> expression;
+
+    SalesAmountType(String viewName, Function<Long, Long> expression) {
+        this.viewName = viewName;
+        this.expression = expression;
+    }
+
+    public long calculate(long amount) {
+        return expression.apply(amount);
+    }
+
+    public String getViewName() {
+        return viewName;
+    }
+}

리뷰: 이것도 시간 부족으로 남은 잔재입니다.

원래는 처음에는  총구매액/행사할인/멤버십할인을 전부 enum으로 관리할려고 했습니다.  
그런데 구현하다보니 그렇게 하면 구현할 수없어서 하나둘씩 리팩터링을 하다보니 멤버십 할인만 남게되었습니다.  

처음 생각한 느낌은 아래와 같습니다.
<img width="626" alt="image" src="https://github.com/user-attachments/assets/573f5877-4bb6-4642-882d-98c60c32b49a">
@@ -0,0 +1,81 @@
+package store.model.item;
+
+import store.error.FileContentException;
+
+public class Item {
+    private static final String KOREAN_REGEX = "^[가-힣]+$";
+    private static final int MAX_PRICE = 100_000_000;
+    public static final int MAX_QUANTITY = 999;
+
+    private final String name;
+    private final int price;
+    private final String promotionName;
+    private int quantity;
+
+    public Item(String name, int price, int quantity, String promotionName) {
+        validate(name, price, quantity);
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotionName = promotionName;
+    }
+
+    private void validate(String name, int price, int quantity) {
+        validateName(name);
+        validatePrice(price);
+        validateQuantity(quantity);
+    }
+
+    private void validateName(String name) {
+        if (name == null || name.trim().isEmpty()) {
+            throw new FileContentException("[ERROR] 상품 이름이 잘못되었습니다.");
+        }
+        if (!name.matches(KOREAN_REGEX)) {
+            throw new FileContentException("[ERROR] 상품 이름은 한글만 입력할 수 있습니다.");
+        }
+    }
+
+    private void validatePrice(int price) {
+        if (price < 0) {
+            throw new FileContentException("[ERROR] 가격은 0원 이상이어야 합니다.");
+        }
+        if (price > MAX_PRICE) {
+            throw new FileContentException("[ERROR] 가격은 최대 1억원 이하까지로만 설정할 수 있습니다.");
+        }
+    }
+
+    private void validateQuantity(int quantity) {
+        if (quantity < 0) {
+            throw new FileContentException("[ERROR] 수량은 0개 이상으로만 설정할 수 있습니다.");
+        }
+        if (quantity > MAX_QUANTITY) {
+            throw new FileContentException("[ERROR] 수량은 최대 999개까지 설정할 수 있습니다.");
+        }
+    }
+
+    void decreaseQuantity(int quantity) {
+        if (quantity < 0) {
+            throw new IllegalArgumentException("[ERROR] 잘못된 입력입니다. 다시 입력해 주세요.");
+        }
+        if (this.quantity == 0) {
+            throw new IllegalArgumentException("[ERROR] 재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요.");
+        }
+        this.quantity -= quantity;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public String getPromotionName() {
+        return promotionName;
+    }
+}

리뷰: 이건 제 실수입니다. ㅜ
인텔리제이에서 단축키로 상수(Command + Option + C)를 만들면 기본값이 public으로 설정되기 때문에 만들고 나서 private으로 수정해주지 않으면 이렇게 됩니다.
@@ -0,0 +1,81 @@
+package store.model.item;
+
+import store.error.FileContentException;
+
+public class Item {
+    private static final String KOREAN_REGEX = "^[가-힣]+$";
+    private static final int MAX_PRICE = 100_000_000;
+    public static final int MAX_QUANTITY = 999;
+
+    private final String name;
+    private final int price;
+    private final String promotionName;
+    private int quantity;
+
+    public Item(String name, int price, int quantity, String promotionName) {
+        validate(name, price, quantity);
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotionName = promotionName;
+    }
+
+    private void validate(String name, int price, int quantity) {
+        validateName(name);
+        validatePrice(price);
+        validateQuantity(quantity);
+    }
+
+    private void validateName(String name) {
+        if (name == null || name.trim().isEmpty()) {
+            throw new FileContentException("[ERROR] 상품 이름이 잘못되었습니다.");
+        }
+        if (!name.matches(KOREAN_REGEX)) {
+            throw new FileContentException("[ERROR] 상품 이름은 한글만 입력할 수 있습니다.");
+        }
+    }
+
+    private void validatePrice(int price) {
+        if (price < 0) {
+            throw new FileContentException("[ERROR] 가격은 0원 이상이어야 합니다.");
+        }
+        if (price > MAX_PRICE) {
+            throw new FileContentException("[ERROR] 가격은 최대 1억원 이하까지로만 설정할 수 있습니다.");
+        }
+    }
+
+    private void validateQuantity(int quantity) {
+        if (quantity < 0) {
+            throw new FileContentException("[ERROR] 수량은 0개 이상으로만 설정할 수 있습니다.");
+        }
+        if (quantity > MAX_QUANTITY) {
+            throw new FileContentException("[ERROR] 수량은 최대 999개까지 설정할 수 있습니다.");
+        }
+    }
+
+    void decreaseQuantity(int quantity) {
+        if (quantity < 0) {
+            throw new IllegalArgumentException("[ERROR] 잘못된 입력입니다. 다시 입력해 주세요.");
+        }
+        if (this.quantity == 0) {
+            throw new IllegalArgumentException("[ERROR] 재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요.");
+        }
+        this.quantity -= quantity;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public String getPromotionName() {
+        return promotionName;
+    }
+}

리뷰: 수량은 스택오버플로우 방지 때문에 했고 가격은 int의 최대값을 안 넘으면서 편의점에서 있을 법한 가격으로 했습니다.
예전에 신문에서 한정판 위스키를 1억원대에 판매한다는 기사를 본적이 있어서 비슷하게 잡아봤습니다.  

다만 상품 품목 개수 제한을 두지 않아서 무제한이면 스택오버플로우가 발생합니다.
@@ -0,0 +1,76 @@
+package store.model.order;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class OrderItem {
+    private static final String DELIMITER = "-";
+    private static final String REGEX = "[\\[\\]]";
+    private static final Pattern EACH_PATTERN = Pattern.compile("\\[([가-힣]+)-(\\d+)]");
+    private static final String EMPTY = "";
+
+    private final String name;
+    private final int quantity;
+    private int promotionAppliedQuantity = 0;
+    private int totalOrderQuantity = 0;
+    private int nonPromotionQuantity = 0;
+    private int price = 0;
+
+    public OrderItem(String item) {
+        validate(item);
+        String[] itemData = parse(item);
+        this.name = itemData[0];
+        this.quantity = Integer.parseInt(itemData[1]);
+    }
+
+    private void validate(String item) {
+        Matcher matcher = EACH_PATTERN.matcher(item);
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.");
+        }
+    }
+
+    private String[] parse(String item) {
+        return item.replaceAll(REGEX, EMPTY).split(DELIMITER);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public void increasePromotionAppliedQuantity(int quantity) {
+        promotionAppliedQuantity += quantity;
+    }
+
+    public void increaseNonPromotionQuantity(int quantity) {
+        nonPromotionQuantity += quantity;
+    }
+
+    public int getNonPromotionQuantity() {
+        return nonPromotionQuantity;
+    }
+
+    public int getPromotionAppliedQuantity() {
+        return promotionAppliedQuantity;
+    }
+
+    public void increaseTotalOrderQuantity(int quantity) {
+        totalOrderQuantity += quantity;
+    }
+
+    public int getTotalOrderQuantity() {
+        return totalOrderQuantity;
+    }
+
+    public void setPrice(int price) {
+        this.price = price;
+    }
+
+    public int getPrice() {
+        return this.price;
+    }
+}

리뷰: 그게 더 좋다고 생각합니다. 👍 
제가봐도 많습니다.
@@ -0,0 +1,77 @@
+package store.view.utils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import store.common.constants.StringConstants;
+
+public class InputParser {
+    public static String inventoryParser(String name, Long price, Long quantity, String promotion) {
+        StringBuilder stringBuilderWithDash = new StringBuilder(giveBlankToEnd(StringConstants.DASH));
+
+        return appendAll(
+                stringBuilderWithDash,
+                giveBlankToEnd(name),
+                giveBlankToEnd(parsePrice(price)),
+                giveBlankToEnd(parseQuantity(quantity)),
+                parsePromotion(promotion)
+        ).toString();
+    }
+
+    public static Map<String, Long> purchaseInputParser(String[] inputArray) {
+        Map<String, Long> purchaseDetail = new HashMap<>();
+        Arrays.stream(inputArray).forEach(input -> {
+            String bracketsRemoved = input.replace("[", "").replace("]", "");
+            String[] inputSplit = bracketsRemoved.split(StringConstants.DASH);
+            purchaseDetail.put(inputSplit[0], Long.parseLong(inputSplit[1]));
+        });
+        return purchaseDetail;
+    }
+
+    public static StringBuilder giveCommaToPrice(String[] splitPrice) {
+        StringBuilder priceWithComma = new StringBuilder();
+        for (int i = splitPrice.length - 1 ; i >= 0; i--) {
+            priceWithComma.insert(0, splitPrice[i]);
+            if ((splitPrice.length - i) % 3 == 0 && i != 0) {
+                priceWithComma.insert(0, StringConstants.COMMA);
+            }
+        }
+        return priceWithComma;
+    }
+
+    private static String giveBlankToEnd(String name) {
+        return name + StringConstants.BLANK;
+    }
+
+    private static String parsePromotion(String promotion) {
+        String promotionNullConverted = promotion;
+        if (Objects.equals(promotion, "null")) {
+            promotionNullConverted = "";
+        }
+        return promotionNullConverted;
+    }
+
+    private static String parseQuantity(Long quantity) {
+        String quantityWithPieces = quantity.toString() + StringConstants.PIECES;
+        if (quantity == 0) {
+            quantityWithPieces = StringConstants.OUT_OF_STOCK;
+        }
+        return quantityWithPieces;
+    }
+
+    private static String parsePrice(Long price) {
+        if (price >= 1000) {
+            String[] splitPrice = price.toString().split("");
+            return giveCommaToPrice(splitPrice).append(StringConstants.WON).toString();
+        }
+        return price + StringConstants.WON;
+    }
+
+    private static StringBuilder appendAll(StringBuilder stringbuilder, String... strings) {
+        for (String string : strings) {
+            stringbuilder.append(string);
+        }
+        return stringbuilder;
+    }
+}

리뷰: `parsePrice` 메서드는 숫자에 천 단위 콤마를 추가하는 과정을 진행하고 있습니다.
자바에서 제공하는 `String.format` 의 `%,d` 를 사용하면 좋을 듯 합니다:)
@@ -0,0 +1,8 @@
+package store.common.constants;
+
+public class AddressConstants {
+    public static final String productFilePath =
+            "/Users/hanwool/WooTeco/java-convenience-store-7-hanwool1643/src/main/resources/products.md";
+    public static final String promotionFilePath =
+            "/Users/hanwool/WooTeco/java-convenience-store-7-hanwool1643/src/main/resources/promotions.md";
+}

리뷰: 이 부분이 파일위치에 따라 문제가 발생할 수도 있을 것 같아요!
절대 경로가 아닌 상대 경로로 변경하는 건 어떨까요?
@@ -0,0 +1,63 @@
+package store.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.util.Map;
+import store.common.constants.ErrorConstants;
+import store.common.constants.MessageConstants;
+import store.common.constants.StringConstants;
+import store.view.utils.InputParser;
+import store.view.utils.InputValidation;
+
+public class InputView {
+
+    public static Map<String, Long> askPurchaseProduct() {
+        System.out.println(MessageConstants.PRODUCT_PURCHASE_MESSAGE);
+        while (true) {
+            try {
+                String input = Console.readLine();
+                String[] inputSplit = InputValidation.validatePurchaseInput(input);
+                System.out.println();
+                return InputParser.purchaseInputParser(inputSplit);
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellPromotionNotApplicable(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_NOT_APPLY_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    private static String getYesOrNoAnswer() {
+        while(true) {
+            try {
+                String answer = Console.readLine();
+                System.out.println();
+                if (!(answer.equals(StringConstants.NO) || answer.equals(StringConstants.YES))) {
+                    throw new IllegalArgumentException(ErrorConstants.INPUT_FORMAT_ERROR_MESSAGE);
+                }
+                return answer;
+            } catch(IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellFreeProductProvide(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_PROVIDE_FREE_PRODUCT_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    public static String askMembershipDiscount() {
+        System.out.println(MessageConstants.MEMBERSHIP_DISCOUNT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+
+    public static String buyAnotherProduct() {
+        System.out.println(MessageConstants.EXIT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+}

리뷰: validation 로직과 parser 로직을 InputView에서 실행하는 것은 InputView에 너무 많은 책임을 부과하는 것 같습니다!
InputView에서는 안내 메시지 출력과 입력을 받는 부분만 남겨두고, 나머지 부분을 controller에 작성하시는 것은 어떠실까요?
@@ -0,0 +1,63 @@
+package store.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.util.Map;
+import store.common.constants.ErrorConstants;
+import store.common.constants.MessageConstants;
+import store.common.constants.StringConstants;
+import store.view.utils.InputParser;
+import store.view.utils.InputValidation;
+
+public class InputView {
+
+    public static Map<String, Long> askPurchaseProduct() {
+        System.out.println(MessageConstants.PRODUCT_PURCHASE_MESSAGE);
+        while (true) {
+            try {
+                String input = Console.readLine();
+                String[] inputSplit = InputValidation.validatePurchaseInput(input);
+                System.out.println();
+                return InputParser.purchaseInputParser(inputSplit);
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellPromotionNotApplicable(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_NOT_APPLY_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    private static String getYesOrNoAnswer() {
+        while(true) {
+            try {
+                String answer = Console.readLine();
+                System.out.println();
+                if (!(answer.equals(StringConstants.NO) || answer.equals(StringConstants.YES))) {
+                    throw new IllegalArgumentException(ErrorConstants.INPUT_FORMAT_ERROR_MESSAGE);
+                }
+                return answer;
+            } catch(IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellFreeProductProvide(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_PROVIDE_FREE_PRODUCT_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    public static String askMembershipDiscount() {
+        System.out.println(MessageConstants.MEMBERSHIP_DISCOUNT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+
+    public static String buyAnotherProduct() {
+        System.out.println(MessageConstants.EXIT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+}

리뷰: 그리고 validation 로직을 parser 로직에 넣으시면 좋을 듯 합니다!
@@ -0,0 +1,77 @@
+package store.view.utils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import store.common.constants.StringConstants;
+
+public class InputParser {
+    public static String inventoryParser(String name, Long price, Long quantity, String promotion) {
+        StringBuilder stringBuilderWithDash = new StringBuilder(giveBlankToEnd(StringConstants.DASH));
+
+        return appendAll(
+                stringBuilderWithDash,
+                giveBlankToEnd(name),
+                giveBlankToEnd(parsePrice(price)),
+                giveBlankToEnd(parseQuantity(quantity)),
+                parsePromotion(promotion)
+        ).toString();
+    }
+
+    public static Map<String, Long> purchaseInputParser(String[] inputArray) {
+        Map<String, Long> purchaseDetail = new HashMap<>();
+        Arrays.stream(inputArray).forEach(input -> {
+            String bracketsRemoved = input.replace("[", "").replace("]", "");
+            String[] inputSplit = bracketsRemoved.split(StringConstants.DASH);
+            purchaseDetail.put(inputSplit[0], Long.parseLong(inputSplit[1]));
+        });
+        return purchaseDetail;
+    }
+
+    public static StringBuilder giveCommaToPrice(String[] splitPrice) {
+        StringBuilder priceWithComma = new StringBuilder();
+        for (int i = splitPrice.length - 1 ; i >= 0; i--) {
+            priceWithComma.insert(0, splitPrice[i]);
+            if ((splitPrice.length - i) % 3 == 0 && i != 0) {
+                priceWithComma.insert(0, StringConstants.COMMA);
+            }
+        }
+        return priceWithComma;
+    }
+
+    private static String giveBlankToEnd(String name) {
+        return name + StringConstants.BLANK;
+    }
+
+    private static String parsePromotion(String promotion) {
+        String promotionNullConverted = promotion;
+        if (Objects.equals(promotion, "null")) {
+            promotionNullConverted = "";
+        }
+        return promotionNullConverted;
+    }
+
+    private static String parseQuantity(Long quantity) {
+        String quantityWithPieces = quantity.toString() + StringConstants.PIECES;
+        if (quantity == 0) {
+            quantityWithPieces = StringConstants.OUT_OF_STOCK;
+        }
+        return quantityWithPieces;
+    }
+
+    private static String parsePrice(Long price) {
+        if (price >= 1000) {
+            String[] splitPrice = price.toString().split("");
+            return giveCommaToPrice(splitPrice).append(StringConstants.WON).toString();
+        }
+        return price + StringConstants.WON;
+    }
+
+    private static StringBuilder appendAll(StringBuilder stringbuilder, String... strings) {
+        for (String string : strings) {
+            stringbuilder.append(string);
+        }
+        return stringbuilder;
+    }
+}

리뷰: 동의합니다
@@ -0,0 +1,8 @@
+package store.common.constants;
+
+public class AddressConstants {
+    public static final String productFilePath =
+            "/Users/hanwool/WooTeco/java-convenience-store-7-hanwool1643/src/main/resources/products.md";
+    public static final String promotionFilePath =
+            "/Users/hanwool/WooTeco/java-convenience-store-7-hanwool1643/src/main/resources/promotions.md";
+}

리뷰: 동의합니다!
@@ -0,0 +1,50 @@
+package store.domain;
+
+import static store.common.constants.NumberConstants.PRODUCT_NAME_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_PRICE_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_PROMOTION_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_QUANTITY_INDEX;
+
+import store.common.constants.ErrorConstants;
+
+public class Product {
+    private final String name;
+    private final Long price;
+    private Long quantity;
+    private final String promotion;
+
+    public Product(String[] productsInfo) {
+        String name = productsInfo[PRODUCT_NAME_INDEX];
+        long price = Long.parseLong(productsInfo[PRODUCT_PRICE_INDEX]);
+        long quantity = Long.parseLong(productsInfo[PRODUCT_QUANTITY_INDEX]);
+        String promotion = productsInfo[PRODUCT_PROMOTION_INDEX];
+
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public void buy(Long quantity) {
+        if (quantity > this.quantity) {
+            throw new IllegalArgumentException(ErrorConstants.INVENTORY_SHORT_ERROR_MESSAGE);
+        }
+        this.quantity -= quantity;
+    }
+
+    public Long getPrice() {
+        return price;
+    }
+
+    public Long getQuantity() {
+        return quantity;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

리뷰: 왜 배열로 넘기는 방식을 택하셨는지 궁금합니다! DTO로 넘기는 경우나 제네릭타입으로 넘기는 경우도 있을거 같아서요!