ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,29 @@
+package store.constants;
+
+public enum OutputPrompts {
+    WELCOME_MESSAGE("\n안녕하세요. W편의점입니다.\n"
+            + "현재 보유하고 있는 상품입니다.\n"),
+
+    PRODUCTS_HAVE_PROMOTION("- %s %s원 %s %s"),
+    PRODUCTS_NO_PROMOTION("- %s %s원 %s"),
+    RECEIPT_HEADER("\n==============W 편의점================\n"
+                 + "상품명\t\t수량\t금액"),
+    RECEIPT_PRODUCTS("%s\t\t%d\t%s\n"),
+    RECEIPT_PROMOTION_HEADER("=============증\t정==============="),
+    RECEIPT_PROMOTION_PRODUCTS("%s\t\t%d\n"),
+    RECEIPT_TOTAL_PRICE_HEADER("===================================="),
+    RECEIPT_TOTAL_PRICE("총구매액\t\t%d\t%s\n"),
+    RECEIPT_PROMOTION_DISCOUNT("행사할인\t\t\t-%s\n"),
+    RECEIPT_MEMBERSHIP_DISCOUNT("멤버십할인\t\t\t-%s\n"),
+    RECEIPT_FINAL_PRICE("내실돈\t\t\t %s\n\n");
+
+    private final String prompts;
+
+    OutputPrompts(String prompts) {
+        this.prompts = prompts;
+    }
+
+    public String getPrompts() {
+        return prompts;
+    }
+}

리뷰: 어떤 상수에서 enum 타입을 사용하시는지 기준이 궁금합니다!
@@ -0,0 +1,84 @@
+package store.controller;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.io.IOException;
+import java.util.Map;
+import store.constants.ErrorMessage;
+import store.constants.StringConstants;
+import store.domain.Cart;
+import store.domain.Products;
+import store.domain.Promotions;
+import store.domain.Purchase;
+import store.domain.TotalPrice;
+import store.service.CartService;
+import store.service.InitService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class StoreController {
+
+    private Products products;
+    private Promotions promotions;
+    private Cart cart;
+
+    private final InitService initService = new InitService();
+    private final PromotionService promotionService = new PromotionService();
+    private final CartService cartService = new CartService();
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+
+    public void run() {
+        String next = StringConstants.YES.getString();
+        initStock();
+        while(next.equals(StringConstants.YES.getString())) {
+            start();
+            checkProducts();
+            result();
+            products = cartService.productsReduce(products, cart);
+            next = inputView.checkMorePurchase();
+        }
+        Console.close();
+    }
+
+    private void initStock() {
+        try {
+            products = initService.saveInitProducts();
+            promotions = initService.saveInitPromotions();
+        } catch (IOException e) {
+            System.out.println(ErrorMessage.SYSTEM_ERROR.getMessage());
+        }
+    }
+
+    private void start() {
+        outputView.productsOutput(this.products);
+        cart = inputView.readItem(products);
+    }
+
+    private void checkProducts() {
+        for (Purchase purchase : cart.getItems()) {
+            promotionService.setPromotion(purchase, products);
+            if (!purchase.getPromotion().equals(StringConstants.NO_PROMOTION.getString())) {
+                promotionService.promotionsAll(purchase, promotions, products, cart);
+            }
+        }
+        addCart();
+    }
+
+    private void addCart() {
+        promotionService.applyAddPurchaseToCart(cart);
+    }
+
+    private void result() {
+        Map<String, Purchase> items = cart.mergeItems(cart.getItems());
+        TotalPrice totalPrice = new TotalPrice(cart.getItems(), products);
+
+        totalPrice.setPromotionPrice(totalPrice.getPromotionSalePrice(cart.getItems(), promotions, products));
+        String answer = inputView.checkMemberShipSale();
+
+        if (answer.equals(StringConstants.YES.getString())) {
+            totalPrice.setMemberShipPrice(totalPrice.getMembershipSalePrice(cart.getItems(), products));
+        }
+        outputView.printReceipt(cart, products, promotions, items, totalPrice);
+    }
+}

리뷰: 내부에서 생성하는것보다 확장성을 위해서 생성자 주입 방식을 활용해보는건 어떨까요?
@@ -0,0 +1,56 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import store.constants.ErrorMessage;
+
+public class Cart {
+
+    private List<Purchase> items;
+
+    public Cart(List<Purchase> items) {
+        this.items = new ArrayList<>();
+        addPurchase(new ArrayList<>(items));
+    }
+
+    public List<Purchase> getItems() {
+        return items;
+    }
+
+    private void addPurchase(List<Purchase> purchases) {
+        for (Purchase purchase : purchases) {
+            if(validateItem(purchase)) {
+                this.items.add(purchase);
+            }
+        }
+    }
+
+    private boolean validateItem(Purchase purchase) {
+        if (items.stream().anyMatch(item -> item.getName().equals(purchase.getName()))) {
+            throw new IllegalArgumentException(ErrorMessage.DUPLICATE_PRODUCT_INPUT_ERROR.getMessage());
+        }
+        return true;
+    }
+
+    public Map<String, Purchase> mergeItems(List<Purchase> purchases) {
+        Map<String, Purchase> itemsMap = new HashMap<>();
+        for (Purchase purchase : purchases) {
+            addItems(purchase, itemsMap);
+        }
+        return itemsMap;
+    }
+
+    private static void addItems(Purchase purchase, Map<String, Purchase> itemsMap) {
+        Purchase existingItem = itemsMap.get(purchase.getName());
+
+        if (existingItem != null) {
+            existingItem.setQuantity(existingItem.getQuantity() + purchase.getQuantity());
+        }
+        if (existingItem == null) {
+            Purchase item = new Purchase(purchase.getName(), purchase.getQuantity(), purchase.getPromotion());
+            itemsMap.put(item.getName(), item);
+        }
+    }
+}

리뷰: 현재 itemsMap 파라미터에 put으로 내부에서 변경을 가하고 있습니다. 저는 이런 로직을 되도록 피하려고 합니다! 그 이유는 현재는 혼자 개발을 해서 로직을 이해하고 있지만 다른 분들과 협업을 해서 처음 이 클래스를 보고 사용하신다면 itemsMap이 수정이 되는지를 모르기 때문에 사용자는 의도치 않은 변경이 일어나 오류를 받을 수 있을거라고 생각합니다!
그래서 만약 이렇게 파라미터의 값을 변경하는 일이 있는 경우 주석이나 메서드 명으로 알려주는 것이 좋다고 생각합니다! 저의 개인적인 생각이니 참고만 부탁드릴게요 ㅎㅎ 😊
@@ -0,0 +1,49 @@
+package store.service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import store.domain.Product;
+import store.domain.Products;
+import store.domain.Promotion;
+import store.domain.Promotions;
+import store.utils.FileRead;
+import store.utils.Split;
+
+public class InitService {
+
+    private static final String PRODUCTS_FILE_NAME = "src/main/resources/products.md";
+    private static final String PROMOTIONS_FILE_NAME = "src/main/resources/promotions.md";
+
+    public Products saveInitProducts() throws IOException {
+        List<String> fileContent = FileRead.readFile(PRODUCTS_FILE_NAME);
+        List<Product> productList = new ArrayList<>();
+        Set<String> productNames = new LinkedHashSet<>();
+
+        saveProducts(fileContent, productNames, productList);
+        Products products = new Products(productList);
+        products.setProductNames(new ArrayList<>(productNames));
+        products.addNoRegularProducts();
+        return products;
+    }
+
+    private static void saveProducts(List<String> fileContent, Set<String> productNames, List<Product> productList) {
+        for (String content : fileContent) {
+            List<String> split = Split.commaSpliter(content);
+            productNames.add(split.get(0));
+            productList.add(new Product(split.get(0), split.get(1), split.get(2), split.get(3)));
+        }
+    }
+
+    public Promotions saveInitPromotions() throws IOException {
+        List<String> fileContent = FileRead.readFile(PROMOTIONS_FILE_NAME);
+        List<Promotion> promotions = new ArrayList<>();
+        for (String content : fileContent) {
+            List<String> split = Split.commaSpliter(content);
+            promotions.add(new Promotion(split.get(0), split.get(1), split.get(2), split.get(3), split.get(4)));
+        }
+        return new Promotions(promotions);
+    }
+}

리뷰: split의 인덱스 값을 상수화하면 좋을거같습니다! 저도 이번에는 상수화를 안했습니다..ㅎ 하지만 다른 분들의 코드를 보고 이 부분을 상수화하면 split의 각 인덱스가 뭘 의미하는지 한눈에 파악하기 쉬워서 앞으로 인덱스 값도 상수화를 하려고 합니다! 민경님 생각은 어떠신가요?
@@ -0,0 +1,19 @@
+package store.constants;
+
+public enum StringConstants {
+
+    ZERO_STRING("0"),
+    NO_PROMOTION("null"),
+    YES("Y"),
+    NO("N");
+
+    private final String string;
+
+    StringConstants(String string) {
+        this.string = string;
+    }
+
+    public String getString() {
+        return string;
+    }
+}

리뷰: zero_string은 어떤 상황에 쓰이는지 정확히 이름을 보고 파악하기 힘든거 같습니다! 아래 처럼 no_promotion 같은 경우는 프로모션이 없을 때 사용하는지 알아서 편했습니다! 아래 상수화 하신거처럼 의미있는 네이밍하시면 좋을거같습니다 ㅎㅎ
@@ -0,0 +1,82 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import store.constants.ErrorMessage;
+import store.constants.StringConstants;
+
+public class Products {
+
+    private List<String> productNames = new ArrayList<>();
+    private final Map<String, Product> promotionProducts = new LinkedHashMap<>();
+    private final Map<String, Product> regularProducts = new LinkedHashMap<>();
+
+    public Products(List<Product> allProducts) {
+        for (Product product : allProducts) {
+            addProduct(product);
+        }
+    }
+
+    public List<String> getProductNames() {
+        return productNames;
+    }
+
+    public Map<String, Product> getPromotionProducts() {
+        return promotionProducts;
+    }
+
+    public Map<String, Product> getRegularProducts() {
+        return regularProducts;
+    }
+
+    public void setProductNames(List<String> productNames) {
+        this.productNames = productNames;
+    }
+
+    private void addProduct(Product product) {
+        if (product.hasPromotion()) {
+            promotionProducts.put(product.getName(), product);
+        }
+        if (!product.hasPromotion()) {
+            regularProducts.put(product.getName(), product);
+        }
+    }
+
+    public void addNoRegularProducts() {
+        for (String productName : productNames) {
+            if (!regularProducts.containsKey(productName)) {
+                Product product = promotionProducts.get(productName);
+                regularProducts.put(productName, new Product(productName, String.valueOf(product.getPrice()),
+                        StringConstants.ZERO_STRING.getString(), StringConstants.NO_PROMOTION.getString()));
+            }
+        }
+    }
+
+    public boolean hasProduct(String productName) {
+        if (!promotionProducts.containsKey(productName) && !regularProducts.containsKey(productName)) {
+            throw new IllegalArgumentException(ErrorMessage.NO_EXSIST_PRODUCT_ERROR.getMessage());
+        }
+        return promotionProducts.containsKey(productName) || regularProducts.containsKey(productName);
+    }
+
+    public void updatePromotionProductQuantity(String name, int quantity) {
+        Product product = promotionProducts.get(name);
+        product.updateQuantity(quantity);
+    }
+
+    public void updateRegularProductQuantity(String name, int quantity) {
+        Product product = regularProducts.get(name);
+        product.updateQuantity(quantity);
+    }
+
+    public boolean hasSufficientStock(String productName, int quantity) {
+        int totalQuantity = regularProducts.get(productName).getQuantity();
+        if (promotionProducts.containsKey(productName)) {
+            totalQuantity += promotionProducts.get(productName).getQuantity();
+        }
+
+        return totalQuantity >= quantity;
+    }
+}

리뷰: 외부로 리스트, 맵처럼 컬렉션을 반환하면 final로 설정한다해도 add, remove 메서드들로 수정을 가할 수가 있습니다. 만약 수정할 의도가 없으시다면 복사를 하여 외부로 넘기는 방법이 좋을거 같습니다!
저는 주로 Collections.unmodifiableMap(promotionProducts); 이렇게 수정을 가하면 예외가 던져지도록 설정합니다!
@@ -0,0 +1,61 @@
+package store.validator;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import store.constants.ErrorMessage;
+import store.constants.StringConstants;
+import store.domain.Cart;
+import store.domain.Products;
+import store.domain.Purchase;
+import store.utils.Split;
+
+public class InputValidator {
+
+    private static final String PATTERN = "^[a-zA-Z가-힣0-9\\-\\[\\]]+$";
+    private static final String VALID_PATTERN = "\\[[a-zA-Z가-힣0-9]+-[0-9]+\\]";
+
+    public static void validateInput(String input) {
+        List<String> items = Split.commaSpliter(input);
+
+        for (String item : items) {
+            if (!item.matches(VALID_PATTERN)) {
+                throw new IllegalArgumentException(ErrorMessage.PRODUCT_BUY_FORM_ERROR.getMessage());
+            }
+        }
+    }
+
+    public static void validatePurchaseForm(List<String> inputs) {
+        for (String input : inputs) {
+            Pattern compiledPattern = Pattern.compile(PATTERN);
+            Matcher matcher = compiledPattern.matcher(input);
+
+            if (!matcher.matches()) {
+                throw new IllegalArgumentException(ErrorMessage.PRODUCT_BUY_FORM_ERROR.getMessage());
+            }
+        }
+    }
+
+    public static void validateHasProduct(Cart cart, Products products) {
+        for (Purchase purchase : cart.getItems()) {
+            if (!products.hasProduct(purchase.getName())) {
+                throw new IllegalArgumentException(ErrorMessage.NO_EXSIST_PRODUCT_ERROR.getMessage());
+            }
+        }
+    }
+
+    public static void validateSufficientStock(Cart cart, Products products) {
+        for (Purchase purchase : cart.getItems()) {
+            if (!products.hasSufficientStock(purchase.getName(), purchase.getQuantity())) {
+                throw new IllegalArgumentException(ErrorMessage.NOT_ENOUGH_QUANTITY_ERROR.getMessage());
+            }
+        }
+    }
+
+    public static void validateAnswer(String answer) {
+        answer = answer.toUpperCase();
+        if (!answer.equals(StringConstants.YES.getString()) && !answer.equals(StringConstants.NO.getString())) {
+            throw new IllegalArgumentException(ErrorMessage.ETC_INPUT_ERROR.getMessage());
+        }
+    }
+}

리뷰: 검증을 한 곳에 모아두는 이유가 있나요??
@@ -0,0 +1,62 @@
+package store.domain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import store.constants.ErrorMessage;
+
+class CartTest {
+
+    private Cart cart;
+    private List<Purchase> purchases;
+
+    @BeforeEach
+    void setUp() {
+        purchases = new ArrayList<>();
+        purchases.add(new Purchase("콜라", 3, "탄산2+1"));
+        purchases.add(new Purchase("사이다", 5, "null"));
+        cart = new Cart(purchases);
+    }
+
+    @Test
+    void 장바구니_생성_테스트() {
+        // given
+        List<Purchase> expectedItems = purchases;
+
+        // then
+        assertThat(cart.getItems().size()).isEqualTo(expectedItems.size());
+        assertThat(cart.getItems()).containsAll(expectedItems);
+    }
+
+    @Test
+    void 중복_아이템_추가_예외_테스트() {
+        // given
+        purchases.add(new Purchase("콜라", 2, "탄산2+1"));
+
+        // when & then
+        assertThatThrownBy(() -> new Cart(purchases))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage(ErrorMessage.DUPLICATE_PRODUCT_INPUT_ERROR.getMessage());
+    }
+
+    @Test
+    void 아이템_병합_테스트() {
+        // given
+        List<Purchase> additionalPurchases = new ArrayList<>(cart.getItems());;
+        additionalPurchases.add(new Purchase("콜라", 2, "탄산2+1"));
+        additionalPurchases.add(new Purchase("물", 4, "null"));
+
+        // when
+        Map<String, Purchase> mergedItems = cart.mergeItems(additionalPurchases);
+
+        // then
+        assertThat(mergedItems.get("콜라").getQuantity()).isEqualTo(5);  // 기존 3 + 추가 2
+        assertThat(mergedItems.containsKey("물")).isTrue();
+        assertThat(mergedItems.get("물").getQuantity()).isEqualTo(4);
+    }
+}
\ No newline at end of file

리뷰: given when then 덕분에 테스트 코드 읽기가 쉽네요!👍
@@ -0,0 +1,25 @@
+package store.constants;
+
+public enum ErrorMessage {
+
+    PRODUCT_BUY_FORM_ERROR("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요."),
+    NO_EXSIST_PRODUCT_ERROR("존재하지 않는 상품 입니다. 다시 입력해 주세요"),
+    NOT_ENOUGH_QUANTITY_ERROR("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    NOT_POSITIVE_INPUT_ERROR("구입할 수량은 1 이상이어야 합니다. 다시 입력해 주세요."),
+    ETC_INPUT_ERROR("잘못된 입력입니다. 다시 입력해주세요."),
+    DUPLICATE_PRODUCT_INPUT_ERROR("같은 상품은 한 번에 여러개 구입합니다. 다시 입력해 주세요."),
+    PRODUCT_ERROR("잘못된 재고입니다."),
+    SYSTEM_ERROR("실행 오류 입니다.");
+
+    private final String prefix = "[ERROR] ";
+    private final String postfix = "\n";
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = prefix + message + postfix;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: 최소 구매 수량이 바뀌면 예외메시지도 바뀌어야 할 것 같아요. 상수를 외부에서 가져오도록 바꾸는 건 어떨까요?!
@@ -0,0 +1,21 @@
+package store.constants;
+
+public enum InputPrompts {
+
+    START_PROMPTS("안녕하세요. W편의점입니다. \n현재 보유하고 있는 상품입니다. \n"),
+    ENTER_PURCHASE_PRODUCTS("구매하실 상품명과 수량을 입력해주세요. (예: [사이다-2],[감자칩-1])"),
+    PROMOTION_QUANTITY_ADD("현재 %s은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)\n"),
+    NOT_ENOUGH_PROMOTION_QUANTITY("현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)\n"),
+    MEMBERSHIP_DISCOUNT("\n멤버십 할인을 받으시겠습니까? (Y/N)"),
+    MORE_PURCHASE("감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)");
+
+    private final String prompt;
+
+    InputPrompts(String prompt) {
+        this.prompt = prompt;
+    }
+
+    public String getPrompt() {
+        return prompt;
+    }
+}

리뷰: 2+2 이벤트같은게 열리면 이 메시지는 잘못될 수도 있을 것 같아요!
@@ -0,0 +1,29 @@
+package store.constants;
+
+public enum OutputPrompts {
+    WELCOME_MESSAGE("\n안녕하세요. W편의점입니다.\n"
+            + "현재 보유하고 있는 상품입니다.\n"),
+
+    PRODUCTS_HAVE_PROMOTION("- %s %s원 %s %s"),
+    PRODUCTS_NO_PROMOTION("- %s %s원 %s"),
+    RECEIPT_HEADER("\n==============W 편의점================\n"
+                 + "상품명\t\t수량\t금액"),
+    RECEIPT_PRODUCTS("%s\t\t%d\t%s\n"),
+    RECEIPT_PROMOTION_HEADER("=============증\t정==============="),
+    RECEIPT_PROMOTION_PRODUCTS("%s\t\t%d\n"),
+    RECEIPT_TOTAL_PRICE_HEADER("===================================="),
+    RECEIPT_TOTAL_PRICE("총구매액\t\t%d\t%s\n"),
+    RECEIPT_PROMOTION_DISCOUNT("행사할인\t\t\t-%s\n"),
+    RECEIPT_MEMBERSHIP_DISCOUNT("멤버십할인\t\t\t-%s\n"),
+    RECEIPT_FINAL_PRICE("내실돈\t\t\t %s\n\n");
+
+    private final String prompts;
+
+    OutputPrompts(String prompts) {
+        this.prompts = prompts;
+    }
+
+    public String getPrompts() {
+        return prompts;
+    }
+}

리뷰: \n은 운영체제에 종속적인 줄바꿈인 것 같아요! 다른 방법이 없을까요!?
@@ -0,0 +1,84 @@
+package store.controller;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.io.IOException;
+import java.util.Map;
+import store.constants.ErrorMessage;
+import store.constants.StringConstants;
+import store.domain.Cart;
+import store.domain.Products;
+import store.domain.Promotions;
+import store.domain.Purchase;
+import store.domain.TotalPrice;
+import store.service.CartService;
+import store.service.InitService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class StoreController {
+
+    private Products products;
+    private Promotions promotions;
+    private Cart cart;
+
+    private final InitService initService = new InitService();
+    private final PromotionService promotionService = new PromotionService();
+    private final CartService cartService = new CartService();
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+
+    public void run() {
+        String next = StringConstants.YES.getString();
+        initStock();
+        while(next.equals(StringConstants.YES.getString())) {
+            start();
+            checkProducts();
+            result();
+            products = cartService.productsReduce(products, cart);
+            next = inputView.checkMorePurchase();
+        }
+        Console.close();
+    }
+
+    private void initStock() {
+        try {
+            products = initService.saveInitProducts();
+            promotions = initService.saveInitPromotions();
+        } catch (IOException e) {
+            System.out.println(ErrorMessage.SYSTEM_ERROR.getMessage());
+        }
+    }
+
+    private void start() {
+        outputView.productsOutput(this.products);
+        cart = inputView.readItem(products);
+    }
+
+    private void checkProducts() {
+        for (Purchase purchase : cart.getItems()) {
+            promotionService.setPromotion(purchase, products);
+            if (!purchase.getPromotion().equals(StringConstants.NO_PROMOTION.getString())) {
+                promotionService.promotionsAll(purchase, promotions, products, cart);
+            }
+        }
+        addCart();
+    }
+
+    private void addCart() {
+        promotionService.applyAddPurchaseToCart(cart);
+    }
+
+    private void result() {
+        Map<String, Purchase> items = cart.mergeItems(cart.getItems());
+        TotalPrice totalPrice = new TotalPrice(cart.getItems(), products);
+
+        totalPrice.setPromotionPrice(totalPrice.getPromotionSalePrice(cart.getItems(), promotions, products));
+        String answer = inputView.checkMemberShipSale();
+
+        if (answer.equals(StringConstants.YES.getString())) {
+            totalPrice.setMemberShipPrice(totalPrice.getMembershipSalePrice(cart.getItems(), products));
+        }
+        outputView.printReceipt(cart, products, promotions, items, totalPrice);
+    }
+}

리뷰: 문구 출력은 outputView에게 책임을 넘겨줘도 좋을 것 같아요!