ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,67 @@
+package store.product.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+import store.error.ErrorMessage;
+import store.product.domain.PurchaseProduct;
+import store.product.dto.PurchaseProductRequest;
+import store.product.service.ProductService;
+import store.product.view.ProductOutputView;
+import store.state.PurchaseState;
+import store.util.LoopTemplate;
+import store.util.StringParser;
+import store.util.StringValidator;
+import store.view.InputView;
+
+public class ProductController {
+
+    private final InputView inputView;
+    private final ProductOutputView outputView;
+    private final ProductService productService;
+
+    public ProductController(final InputView inputView, final ProductOutputView outputView,
+                             final ProductService productService) {
+        this.inputView = inputView;
+        this.outputView = outputView;
+        this.productService = productService;
+    }
+
+    public void run() {
+        responseProductStatuses();
+        final List<PurchaseProduct> purchaseProducts = requestPurchaseProduct();
+        final PurchaseState purchaseState = PurchaseState.getInstance();
+        purchaseState.updatePurchaseProducts(purchaseProducts);
+    }
+
+    private void responseProductStatuses() {
+        outputView.printIntro();
+        outputView.printProductStatus(productService.getProductStatuses());
+    }
+
+    private List<PurchaseProduct> requestPurchaseProduct() {
+        return LoopTemplate.tryCatchLoop(() -> {
+            outputView.printAskPurchaseProduct();
+            final List<String> products = inputView.readProducts();
+            final List<PurchaseProductRequest> purchaseProductRequests = generatePurchaseProductRequests(products);
+            return productService.generatePurchaseProducts(purchaseProductRequests);
+        });
+    }
+
+    private List<PurchaseProductRequest> generatePurchaseProductRequests(final List<String> values) {
+        final List<PurchaseProductRequest> purchaseProductRequests = new ArrayList<>();
+        for (String value : values) {
+            StringValidator.validateFormat(value, ErrorMessage.INVALID_FORMAT_PRODUCT_AND_QUANTITY);
+            final String product = StringParser.removePattern(value.trim(), "[\\[\\]]");
+            purchaseProductRequests.add(createPurchaseProductRequest(product));
+        }
+        return purchaseProductRequests;
+    }
+
+    private PurchaseProductRequest createPurchaseProductRequest(final String product) {
+        final String[] splitProduct = product.split("-");
+        final String name = splitProduct[0];
+        final int quantity = StringParser.parseToNumber(splitProduct[1]);
+        return new PurchaseProductRequest(name, quantity);
+    }
+
+}

리뷰: @plan11plan 

replaceAll()은 정규식을 사용하기 때문에 다양한 패턴을 변환하기 좋더라구요 ㅎㅎ!
@@ -0,0 +1,68 @@
+package store.product.domain;
+
+import java.util.Objects;
+import store.promotion.domain.Promotion;
+
+public class Product {
+
+    private static final String BLANK = "";
+    private static final int ZERO  = 0;
+    private final long id;
+    private final ProductInfo productInfo;
+    private int quantity;
+
+    public Product(final Long id, final ProductInfo productInfo, final int quantity) {
+        this.id = id;
+        this.productInfo = productInfo;
+        this.quantity = quantity;
+    }
+
+    public Long getId() {
+        return this.id;
+    }
+
+    public void updateQuantity(final int quantity) {
+        this.quantity = quantity;
+    }
+
+    public String getPromotionName() {
+        final Promotion promotion = productInfo.promotion();
+        if (Objects.equals(promotion, null)) {
+            return BLANK;
+        }
+        return promotion.getPromotionName();
+    }
+
+    public boolean canReceiveMorePromotion(final int quantity) {
+        return productInfo.promotion().canPromotion(quantity);
+    }
+
+
+    public int calculateRemainingQuantity(final int quantity) {
+        final Promotion promotion = productInfo.promotion();
+        if (!Objects.equals(promotion, null)) {
+            return promotion.getRemainingQuantity(quantity);
+        }
+        return ZERO;
+    }
+
+    public int getBuyQuantity() {
+        return productInfo.promotion().getBuy();
+    }
+
+    public int getPrice() {
+        return productInfo.price();
+    }
+
+    public String getName() {
+        return productInfo.name();
+    }
+
+    public Promotion getPromotion() {
+        return productInfo.promotion();
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+}

리뷰: @plan11plan 

저는 `product.md`, `promotion.md` 이 두 파일이  마치 상품과 프로모션을 저장하고 있는 DB처럼 느껴졌거든요.

그래서 이 데이터들과 매핑할 수 있는 엔티티 객체가 필요해서 Product, Promotion 클래스를 두었고,

Product만 Promotion을 참조하는 단방향 다대일 관계로 풀어냈어요~

그리고 나중에 보니까 Product의 필드 수가 너무 많아져서 일부는 ProductInfo VO로 따로 빼서 관리를 하게끔 했어요~
@@ -0,0 +1,42 @@
+package store.product.domain;
+
+public class ProductQuantity {
+
+    private int normalQuantity;
+    private int promotionQuantity;
+    private int nonPromotionQuantity;
+
+    public ProductQuantity(final int normalQuantity, final int promotionQuantity, final int nonPromotionQuantity) {
+        this.normalQuantity = normalQuantity;
+        this.promotionQuantity = promotionQuantity;
+        this.nonPromotionQuantity = nonPromotionQuantity;
+    }
+
+    public int calculateTotalQuantity() {
+        return normalQuantity + promotionQuantity + nonPromotionQuantity;
+    }
+
+    public int getNormalQuantity() {
+        return normalQuantity;
+    }
+
+    public int getPromotionQuantity() {
+        return promotionQuantity;
+    }
+
+    public int getNonPromotionQuantity() {
+        return nonPromotionQuantity;
+    }
+
+    public void updateNormalQuantity(final int normalQuantity) {
+        this.normalQuantity = normalQuantity;
+    }
+
+    public void updatePromotionQuantity(final int promotionQuantity) {
+        this.promotionQuantity = promotionQuantity;
+    }
+
+    public void updateNonPromotionQuantity(final int nonPromotionQuantity) {
+        this.nonPromotionQuantity = nonPromotionQuantity;
+    }
+}

리뷰: @plan11plan 

안그래도 요부분은 불변으로 리팩토링 해야겠다 라는 생각은 있었거든요 ㅎㅎ!

이거 외에도 불변으로 해야하는 포인트들이 몇군데 더 있어서 함께 리팩토링 할 예정이에요~!
@@ -0,0 +1,34 @@
+package store;
+
+import camp.nextstep.edu.missionutils.Console;
+import store.payment.controller.PaymentController;
+import store.product.controller.ProductController;
+import store.promotion.controller.PromotionController;
+import store.state.PurchaseState;
+
+public class Convenience {
+
+    private final ProductController productController;
+    private final PromotionController promotionController;
+    private final PaymentController paymentController;
+
+    public Convenience(final ProductController productController, final PromotionController promotionController,
+                       final PaymentController paymentController) {
+        this.productController = productController;
+        this.promotionController = promotionController;
+        this.paymentController = paymentController;
+    }
+
+    public void purchase() {
+        do {
+            productController.run();
+            promotionController.run();
+            paymentController.run();
+        } while (isRetryPurchase());
+        Console.close();
+    }
+
+    private boolean isRetryPurchase() {
+        return PurchaseState.getInstance().isRetryPurchase();
+    }
+}

리뷰: 싱글톤에 대해 알아보고 싶어서 질문해요.
저는 싱글톤을 유지하는 객체는 동시성 문제 때문에 상태를 갖는 필드를 지양해야 한다고 알고 있어요.
그런데 PurchaseState의 경우 purchaseProducts와 isRetryPurchase와 같이 상태를 가질 수 있는 필드가 존재한다고 생각해요.
이렇게 설계하였을 때 동시성 문제는 생기지 않나요?
발생하지 않는다면 왜 그런 건가요?
@@ -0,0 +1,34 @@
+package store;
+
+import camp.nextstep.edu.missionutils.Console;
+import store.payment.controller.PaymentController;
+import store.product.controller.ProductController;
+import store.promotion.controller.PromotionController;
+import store.state.PurchaseState;
+
+public class Convenience {
+
+    private final ProductController productController;
+    private final PromotionController promotionController;
+    private final PaymentController paymentController;
+
+    public Convenience(final ProductController productController, final PromotionController promotionController,
+                       final PaymentController paymentController) {
+        this.productController = productController;
+        this.promotionController = promotionController;
+        this.paymentController = paymentController;
+    }
+
+    public void purchase() {
+        do {
+            productController.run();
+            promotionController.run();
+            paymentController.run();
+        } while (isRetryPurchase());
+        Console.close();
+    }
+
+    private boolean isRetryPurchase() {
+        return PurchaseState.getInstance().isRetryPurchase();
+    }
+}

리뷰: `PurchaseState`에서 `getInstatnce`를 사용하지 않고 내부에서 재구매를 할 것인가에 대한 메서드를 선언해 사용하면, 디미터의 법칙을 지킬 수 있을 것 같은데 여기에 대한 수달님의 의견이 궁금합니다!
@@ -0,0 +1,73 @@
+package view;
+
+import camp.nextstep.edu.missionutils.Console;
+
+public class InputView {
+    private static final String[] YES_OPTIONS = {"Y", "y"};
+    private static final String[] NO_OPTIONS = {"N", "n"};
+
+    private static final String ITEM_INPUT_PROMPT = "구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])";
+    private static final String CONFIRM_FREE_ITEM_ADDITION_PROMPT = "현재 %s은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)";
+    private static final String CONFIRM_PURCHASE_WITHOUT_PROMOTION_PROMPT = "현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)";
+    private static final String CONFIRM_USE_MEMBERSHIP_PROMPT = "멤버십 할인을 받으시겠습니까? (Y/N)";
+    private static final String CONFIRM_ADDITIONAL_PURCHASE_PROMPT = "감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)";
+    private static final String ERROR_INVALID_INPUT = "[ERROR] 올바른 값을 입력해 주세요. (Y/N)";
+
+    public static String readItem() {
+        System.out.print(System.lineSeparator());
+        System.out.println(ITEM_INPUT_PROMPT);
+        return Console.readLine();
+    }
+
+    public static String confirmFreeItemAddition(String productName) {
+        System.out.print(System.lineSeparator());
+        System.out.printf(CONFIRM_FREE_ITEM_ADDITION_PROMPT + "%n", productName);
+        return validateYesOrNo();
+    }
+
+    public static String confirmPurchaseWithoutPromotion(String productName, int quantity) {
+        System.out.print(System.lineSeparator());
+        System.out.printf(CONFIRM_PURCHASE_WITHOUT_PROMOTION_PROMPT + "%n", productName, quantity);
+        return validateYesOrNo();
+    }
+
+    public static String confirmUseMembership() {
+        System.out.print(System.lineSeparator());
+        System.out.println(CONFIRM_USE_MEMBERSHIP_PROMPT);
+        return validateYesOrNo();
+    }
+
+    public static String confirmAdditionalPurchase() {
+        System.out.print(System.lineSeparator());
+        System.out.println(CONFIRM_ADDITIONAL_PURCHASE_PROMPT);
+        return validateYesOrNo();
+    }
+
+    private static String validateYesOrNo() {
+        while (true) {
+            String input = Console.readLine().trim();
+            if (isYes(input) || isNo(input)) {
+                return input.toUpperCase();
+            }
+            System.out.println(ERROR_INVALID_INPUT);
+        }
+    }
+
+    private static boolean isYes(String input) {
+        for (String yesOption : YES_OPTIONS) {
+            if (yesOption.equals(input)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isNo(String input) {
+        for (String noOption : NO_OPTIONS) {
+            if (noOption.equals(input)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file

리뷰: 사용자의 응답을 판단하는것도 비즈니스 로직중 하나라고 생각을 해요

왜냐하면 응답에 따라서 프로모션, 멤버십 혜택등이 달라지니까요

그래서 요부분은 서비스에서 담당하는게 좋지 않을까 생각해요

범석님 의견은 어떠세요 ??
@@ -0,0 +1,32 @@
+package domain;
+
+public enum PromotionType {
+    NONE(1, 0),
+    BUY_ONE_GET_ONE(1, 1),
+    BUY_TWO_GET_ONE(2, 1);
+
+    private final int buyCount;
+    private final int giveawayCount;
+
+    PromotionType(int buyCount, int giveawayCount) {
+        this.buyCount = buyCount;
+        this.giveawayCount = giveawayCount;
+    }
+
+    public int getDivisor() {
+        return this.buyCount + this.giveawayCount;
+    }
+
+    public int getBuyCount() {
+        return this.buyCount;
+    }
+
+    public static PromotionType fromBuyAndGet(int buy, int get) {
+        for (PromotionType type : PromotionType.values()) {
+            if (type.buyCount == buy && type.giveawayCount == get) {
+                return type;
+            }
+        }
+        return NONE;
+    }
+}
\ No newline at end of file

리뷰: 프로모션 타입을 미리 정하신 이유가 궁금합니다.
프로모션 이름은 같고 내용이 달라질 수 있으니까요.
@@ -0,0 +1,61 @@
+package domain;
+
+import java.util.ArrayList;
+import java.util.List;
+import view.OutputView;
+
+public class Receipt {
+    private static final int INITIAL_AMOUNT = 0;
+    private static final int INITIAL_QUANTITY = 0;
+
+    private final List<PurchaseProduct> purchasedItems;
+    private final List<PurchaseProduct> promoItems;
+    private int totalAmount;
+    private int promotionDiscount;
+    private int membershipDiscount;
+    private int totalGeneralAmount;
+    private int totalQuantity;
+
+    public Receipt() {
+        this.purchasedItems = new ArrayList<>();
+        this.promoItems = new ArrayList<>();
+        this.totalAmount = INITIAL_AMOUNT;
+        this.promotionDiscount = INITIAL_AMOUNT;
+        this.membershipDiscount = INITIAL_AMOUNT;
+        this.totalGeneralAmount = INITIAL_AMOUNT;
+        this.totalQuantity = INITIAL_QUANTITY;
+
+    }
+
+    public void applyPromoItem(PurchaseProduct promoProduct, int divisor) {
+        promoItems.add(promoProduct);
+        int promoAmount = calculateAmount(promoProduct);
+        promotionDiscount += promoAmount;
+        totalGeneralAmount -= promoAmount * divisor;
+    }
+
+    public void applyGeneralItem(PurchaseProduct purchaseProduct) {
+        purchasedItems.add(purchaseProduct);
+        int purchaseAmount = calculateAmount(purchaseProduct);
+        totalAmount += purchaseAmount;
+        totalGeneralAmount += purchaseAmount;
+        totalQuantity += purchaseProduct.getQuantity();
+    }
+
+    public int calculateAmount(PurchaseProduct product) {
+        return product.getPrice() * product.getQuantity();
+    }
+
+
+    public void useMembership(Membership membership) {
+        membershipDiscount = membership.useMembership(totalGeneralAmount);
+    }
+
+    public void print() {
+        OutputView.printReceipt(purchasedItems);
+        if (!promoItems.isEmpty()) {
+            OutputView.printPromotionReceipt(promoItems);
+        }
+        OutputView.printReceiptSummary(totalAmount, totalQuantity, promotionDiscount, membershipDiscount);
+    }
+}

리뷰: 도메인 객체에서 바로 View를 호출하는 것은 Layer를 나눈 의미가 사라진다고 생각해요.
Controller에서 이 기능을 위임시키는 방법도 있으니 한번 고려해보시는 것을 추천드립니다.
@@ -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 + "개";
+    }
+}

리뷰: ``StockManageService``에서도 md파일에 접근하고 있고, ``OutputView``에서도 md파일에 접근하고 있네요.
개인적으로 저는 이번 미션을 진행하면서 md파일 = 데이터베이스 라고 생각하고 코드를 작성했는데, 만약 저와 같은 접근이라면 뷰에서 데이터베이스에 접근하는 것이 옳은 방법일까요?🤔
하영님께서는 md파일을 어떻게 생각하시고 접근하셨을까요?
@@ -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;
+    }
+}

리뷰: ```suggestion
            Product product = new Product(inputProduct);
```
이렇게 네이밍한다면 헷갈리지도 않고 ``pro``라고 줄일지 않아도 되지 않아서 좋을 것 같아요!
@@ -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;
+    }
+}

리뷰: 매개변수로 ``input``을 받는 ``validate``와 ``product``를 받는 ``validate``를 동일한 메서드명으로 명명하신 것에 의도가 있으셨을까요?
리뷰이 입장에서는 ``validateInput``, ``validateDuplicate``와 같은 메서드명이었다면 오잉? 같은 왜 같은 메서드명이지? 라는 생각이 들지 않았을 것 같아서요!
@@ -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;
+    }
+}

리뷰: 생성자 내부에서 0으로 초기화해주시지 않고 ``init()`` 메서드로 초기화해주신 이유가 있을까요~?