ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,77 @@
+package store.global.util;
+
+import static store.global.constant.ErrorMessage.INVALID_INPUT_PURCHASE;
+import static store.global.validation.CommonValidator.validateBlank;
+import static store.global.validation.CommonValidator.validateNotNumeric;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PurchaseParser {
+
+    private static final Pattern PRODUCT_PATTERN = Pattern.compile("^\\[(.*?)-(\\d+)]$");
+    private static final Pattern INVALID_NAME_PATTERN = Pattern.compile(".*[-\\[\\]].*");
+
+    public static Map<String, Integer> parseInputProduct(String input) {
+        validateInput(input);
+
+        String[] splitProducts = splitInput(input);
+        LinkedHashMap<String, Integer> product = new LinkedHashMap<>();
+
+        for (String productString : splitProducts) {
+            processProductString(productString, product);
+        }
+        return product;
+    }
+
+    private static String[] splitInput(String input) {
+        return input.split(",");
+    }
+
+    private static void processProductString(String productString, Map<String, Integer> product) {
+        String name = extractProductName(productString);
+        String quantity = extractProductQuantity(productString);
+        addProductToMap(name, quantity, product);
+    }
+
+    private static String extractProductName(String productString) {
+        Matcher matcher = matchProductPattern(productString);
+        String name = matcher.group(1);
+        validateName(name);
+        return name;
+    }
+
+    private static String extractProductQuantity(String productString) {
+        Matcher matcher = matchProductPattern(productString);
+        String quantity = matcher.group(2);
+        validateNotNumeric(quantity, INVALID_INPUT_PURCHASE);
+        return quantity;
+    }
+
+    private static Matcher matchProductPattern(String productString) {
+        Matcher matcher = PRODUCT_PATTERN.matcher(productString);
+        if (!matcher.find()) {
+            throw new IllegalArgumentException(INVALID_INPUT_PURCHASE.getMessage());
+        }
+        return matcher;
+    }
+
+    private static void addProductToMap(String name, String quantity, Map<String, Integer> product) {
+        product.merge(name, Integer.parseInt(quantity), Integer::sum);
+    }
+
+    private static void validateName(String name) {
+        validateBlank(name, INVALID_INPUT_PURCHASE);
+        if (INVALID_NAME_PATTERN.matcher(name).matches()) {
+            throw new IllegalArgumentException(INVALID_INPUT_PURCHASE.getMessage());
+        }
+    }
+
+    private static void validateInput(String input) {
+        if (!input.matches("^\\[.*]$")) {
+            throw new IllegalArgumentException(INVALID_INPUT_PURCHASE.getMessage());
+        }
+    }
+}

리뷰: 공통 피드백에서 변수 이름에 자료형을 넣지 말라고 하더라구요!
@@ -0,0 +1,15 @@
+package store.domain.product;
+
+public class Name {
+
+    private final String name;
+
+    public Name(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}

리뷰: Name클래스의 용도가 궁금해요!
@@ -0,0 +1,89 @@
+package store.domain;
+
+import static store.global.constant.MessageConstant.FORMAT;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PaymentProductList {
+
+    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("###,###");
+
+    private List<PaymentProduct> paymentProducts;
+    private int totalPrice = 0;
+    private int totalPromotion = 0;
+    private int membershipPrice = 0;
+
+    public PaymentProductList() {
+        this.paymentProducts = new ArrayList<>();
+    }
+
+    public void addPaymentProduct(PaymentProduct product) {
+        paymentProducts.add(product);
+    }
+
+    public List<Integer> getAllProductQuantities() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::getQuantity)
+                .toList();
+    }
+
+    public List<String> infoProduct() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::toString)
+                .toList();
+    }
+
+    public List<String> infoPromotion() {
+        return paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .map(PaymentProduct::buildPromotion)
+                .toList();
+    }
+
+    public List<String> finalResult() {
+        List<String> result = new ArrayList<>();
+        result.add(String.format(FORMAT.getMessage(), "총구매액", totalQuantity(), totalPrice()));
+        result.add(String.format(FORMAT.getMessage(), "행사할인", "", totalPromotionPrice()));
+        result.add(String.format(FORMAT.getMessage(), "멤버십할인", "", "-" + PRICE_FORMAT.format(membershipPrice)));
+        result.add(String.format(FORMAT.getMessage(), "내실돈", "",
+                PRICE_FORMAT.format(totalPrice - totalPromotion - membershipPrice)));
+
+        return result;
+    }
+
+    private int totalQuantity() {
+        return paymentProducts.stream()
+                .mapToInt(PaymentProduct::getQuantity)
+                .sum();
+    }
+
+    private String totalPrice() {
+        this.totalPrice = paymentProducts.stream()
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum();
+        return PRICE_FORMAT.format(totalPrice);
+    }
+
+    private String totalPromotionPrice() {
+        this.totalPromotion = paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .mapToInt(PaymentProduct::getPromotionPrice)
+                .sum();
+        return "-" + PRICE_FORMAT.format(totalPromotion);
+    }
+
+    public void applyMembershipDiscount() {
+        int price = (int) (paymentProducts.stream()
+                .filter(product -> !product.isPromotion())
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum() * 0.3);
+
+        if (price > 8000) {
+            price = 8000;
+        }
+
+        this.membershipPrice = price;
+    }
+}

리뷰: 최대 할인 금액이 넘어섰는지 확인하는 메서드로 따로 분리를 했으면 더 좋았을 것 같다는 생각이 들었어요!
@@ -0,0 +1,46 @@
+package store.domain;
+
+import java.time.LocalDate;
+
+public class Promotions {
+
+    private final String name;
+    private final int buy;
+    private final int get;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+
+    public Promotions(String name, String buy, String get, String startDate, String endDate) {
+        this.name = name;
+        this.buy = Integer.parseInt(buy);
+        this.get = Integer.parseInt(get);
+        this.startDate = LocalDate.parse(startDate);
+        this.endDate = LocalDate.parse(endDate);
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    public boolean hasName(String name) {
+        return this.name.equals(name);
+    }
+
+    public boolean isPromotionApplicable(int quantity) {
+        return calculateRemainingItems(quantity) >= get;
+    }
+
+    public int calculateRemainingItems(int quantity) {
+        return quantity % (buy + get);
+    }
+
+    public int calculatePromotionUnits(int quantity) {
+        return quantity / (buy + get);
+    }
+
+    public boolean isWithinPromotionPeriod(LocalDate date) {
+        return (date.isEqual(startDate) || date.isAfter(startDate)) &&
+                (date.isEqual(endDate) || date.isBefore(endDate));
+    }
+}

리뷰: 혹시, Name 타입으로 하지 않으신 이유가 있나용??
@@ -0,0 +1,39 @@
+package store.domain.product;
+
+import static store.global.constant.ErrorMessage.INVALID_PRICE_NUMERIC;
+import static store.global.constant.ErrorMessage.INVALID_PRICE_OUT_OF_RANGE;
+import static store.global.validation.CommonValidator.validateNotNumeric;
+
+import java.text.DecimalFormat;
+
+public class Price {
+
+    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("###,###");
+    private static final int MINIMUM_PRICE = 1;
+    private static final int MAXIMUM_PRICE = 1_000_000;
+    private static final String UNIT = "원";
+
+    private final int price;
+
+    public Price(final String inputPrice) {
+        validateNotNumeric(inputPrice, INVALID_PRICE_NUMERIC);
+        int price = Integer.parseInt(inputPrice);
+        validateRange(price);
+        this.price = price;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    @Override
+    public String toString() {
+        return DECIMAL_FORMAT.format(price) + UNIT;
+    }
+
+    private void validateRange(final int price) {
+        if (price < MINIMUM_PRICE || price > MAXIMUM_PRICE) {
+            throw new IllegalArgumentException(INVALID_PRICE_OUT_OF_RANGE.getMessage());
+        }
+    }
+}

리뷰: 동일한 형식의 상수를 Quantity에서도 보았어요!
혹시, 공통적으로 사용하는 상수는 따로 분리를 하시는 것은 어떻게 생각하시나요?
@@ -0,0 +1,58 @@
+package store.view;
+
+import static store.global.constant.MessageConstant.FORMAT;
+import static store.global.constant.MessageConstant.NEW_LINE;
+import static store.global.constant.MessageConstant.OUTPUT_HEADER_PROMOTION;
+import static store.global.constant.MessageConstant.OUTPUT_HEADER_SEPARATOR;
+import static store.global.constant.MessageConstant.OUTPUT_HEADER_STORE;
+import static store.global.constant.MessageConstant.OUTPUT_PRODUCT_INFO;
+import static store.global.constant.MessageConstant.OUTPUT_PRODUCT_PREFIX;
+import static store.global.constant.MessageConstant.OUTPUT_PROMOTION_ADD;
+import static store.global.constant.MessageConstant.OUTPUT_PROMOTION_NOT_APPLIED;
+import static store.global.constant.MessageConstant.OUTPUT_WELCOME;
+
+import java.util.List;
+
+public class OutputView {
+
+    public void printWelcomeMessage() {
+        System.out.println(OUTPUT_WELCOME.getMessage());
+        System.out.println(OUTPUT_PRODUCT_INFO.getMessage() + NEW_LINE.getMessage());
+    }
+
+    public void printProductsInfo(List<String> products) {
+        products.forEach(product ->
+                System.out.println(OUTPUT_PRODUCT_PREFIX.getMessage() + product)
+        );
+    }
+
+    public void printPromotionNotApplied(String name, int quantity) {
+        System.out.printf(NEW_LINE.getMessage() + OUTPUT_PROMOTION_NOT_APPLIED.getMessage() + NEW_LINE.getMessage(),
+                name, quantity);
+    }
+
+    public void printPromotionAddition(String name) {
+        System.out.printf(NEW_LINE.getMessage() + OUTPUT_PROMOTION_ADD.getMessage() + NEW_LINE.getMessage(), name);
+    }
+
+    public void printPaymentInfoResult(List<String> infos) {
+        System.out.printf(NEW_LINE.getMessage());
+        System.out.println(OUTPUT_HEADER_STORE.getMessage());
+        System.out.printf(FORMAT.getMessage() + NEW_LINE.getMessage(), "상품명", "수량", "금액");
+        infos.forEach(System.out::println);
+    }
+
+    public void printPromotionInfoResult(List<String> strings) {
+        System.out.println(OUTPUT_HEADER_PROMOTION.getMessage());
+        strings.forEach(System.out::println);
+    }
+
+    public void printFinalResult(List<String> result) {
+        System.out.println(OUTPUT_HEADER_SEPARATOR.getMessage());
+        result.forEach(System.out::println);
+    }
+
+    public void printErrorMessage(String message) {
+        System.out.println(message);
+    }
+}

리뷰: 안녕하세요!

혹시 아래와 같이 포맷을 설정하셨을 때, 띄어쓰기 때문에 출력이 올바르게 정렬이 가능하셨나요?
4주 차 진행하면서 영수증 출력 부분을 보기 좋게 정리하는 것이 요구사항이었어서 많이 찾아봤습니다!

그 과정에서 알게 된 점은, 한국어는 2글자로 취급되지만, 띄어쓰기와 영어는 1글자로 취급된다는 것입니다. 그래서 띄어쓰기를 한글처럼 2글자 역할을 할 수 있는 방법이 있을까 고민하다가, \u3000 (유니코드 공백)을 알게 되었습니다!

예를 들어, 아래와 같이 사용할 수 있습니다:
`String name = String.format("%-14s", "총구매액").replace(" ", "\u3000");`
이 방법을 사용하면 한글처럼 띄어쓰기를 2글자 취급할 수 있어서 정렬 문제를 해결할 수 있습니다! 😊
@@ -0,0 +1,89 @@
+package store.domain;
+
+import static store.global.constant.MessageConstant.FORMAT;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PaymentProductList {
+
+    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("###,###");
+
+    private List<PaymentProduct> paymentProducts;
+    private int totalPrice = 0;
+    private int totalPromotion = 0;
+    private int membershipPrice = 0;
+
+    public PaymentProductList() {
+        this.paymentProducts = new ArrayList<>();
+    }
+
+    public void addPaymentProduct(PaymentProduct product) {
+        paymentProducts.add(product);
+    }
+
+    public List<Integer> getAllProductQuantities() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::getQuantity)
+                .toList();
+    }
+
+    public List<String> infoProduct() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::toString)
+                .toList();
+    }
+
+    public List<String> infoPromotion() {
+        return paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .map(PaymentProduct::buildPromotion)
+                .toList();
+    }
+
+    public List<String> finalResult() {
+        List<String> result = new ArrayList<>();
+        result.add(String.format(FORMAT.getMessage(), "총구매액", totalQuantity(), totalPrice()));
+        result.add(String.format(FORMAT.getMessage(), "행사할인", "", totalPromotionPrice()));
+        result.add(String.format(FORMAT.getMessage(), "멤버십할인", "", "-" + PRICE_FORMAT.format(membershipPrice)));
+        result.add(String.format(FORMAT.getMessage(), "내실돈", "",
+                PRICE_FORMAT.format(totalPrice - totalPromotion - membershipPrice)));
+
+        return result;
+    }
+
+    private int totalQuantity() {
+        return paymentProducts.stream()
+                .mapToInt(PaymentProduct::getQuantity)
+                .sum();
+    }
+
+    private String totalPrice() {
+        this.totalPrice = paymentProducts.stream()
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum();
+        return PRICE_FORMAT.format(totalPrice);
+    }
+
+    private String totalPromotionPrice() {
+        this.totalPromotion = paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .mapToInt(PaymentProduct::getPromotionPrice)
+                .sum();
+        return "-" + PRICE_FORMAT.format(totalPromotion);
+    }
+
+    public void applyMembershipDiscount() {
+        int price = (int) (paymentProducts.stream()
+                .filter(product -> !product.isPromotion())
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum() * 0.3);
+
+        if (price > 8000) {
+            price = 8000;
+        }
+
+        this.membershipPrice = price;
+    }
+}

리뷰: 안녕하세요!

저도 공통 피드백에서 도메인 객체는 출력과 관련된 로직을 최대한 배제하고,
필요한 경우 toString을 오버라이드하는 정도로 제한하는 것이 좋다고 본 것 같습니다.

현재 해당 부분은 도메인에서 출력 로직을 처리하고 있는 것으로 보여요!
view에서 처리하는 방식이 더 좋은 것 같은데 어떻게 생각하시나요?
@@ -0,0 +1,58 @@
+package store.view;
+
+import static store.global.constant.MessageConstant.FORMAT;
+import static store.global.constant.MessageConstant.NEW_LINE;
+import static store.global.constant.MessageConstant.OUTPUT_HEADER_PROMOTION;
+import static store.global.constant.MessageConstant.OUTPUT_HEADER_SEPARATOR;
+import static store.global.constant.MessageConstant.OUTPUT_HEADER_STORE;
+import static store.global.constant.MessageConstant.OUTPUT_PRODUCT_INFO;
+import static store.global.constant.MessageConstant.OUTPUT_PRODUCT_PREFIX;
+import static store.global.constant.MessageConstant.OUTPUT_PROMOTION_ADD;
+import static store.global.constant.MessageConstant.OUTPUT_PROMOTION_NOT_APPLIED;
+import static store.global.constant.MessageConstant.OUTPUT_WELCOME;
+
+import java.util.List;
+
+public class OutputView {
+
+    public void printWelcomeMessage() {
+        System.out.println(OUTPUT_WELCOME.getMessage());
+        System.out.println(OUTPUT_PRODUCT_INFO.getMessage() + NEW_LINE.getMessage());
+    }
+
+    public void printProductsInfo(List<String> products) {
+        products.forEach(product ->
+                System.out.println(OUTPUT_PRODUCT_PREFIX.getMessage() + product)
+        );
+    }
+
+    public void printPromotionNotApplied(String name, int quantity) {
+        System.out.printf(NEW_LINE.getMessage() + OUTPUT_PROMOTION_NOT_APPLIED.getMessage() + NEW_LINE.getMessage(),
+                name, quantity);
+    }
+
+    public void printPromotionAddition(String name) {
+        System.out.printf(NEW_LINE.getMessage() + OUTPUT_PROMOTION_ADD.getMessage() + NEW_LINE.getMessage(), name);
+    }
+
+    public void printPaymentInfoResult(List<String> infos) {
+        System.out.printf(NEW_LINE.getMessage());
+        System.out.println(OUTPUT_HEADER_STORE.getMessage());
+        System.out.printf(FORMAT.getMessage() + NEW_LINE.getMessage(), "상품명", "수량", "금액");
+        infos.forEach(System.out::println);
+    }
+
+    public void printPromotionInfoResult(List<String> strings) {
+        System.out.println(OUTPUT_HEADER_PROMOTION.getMessage());
+        strings.forEach(System.out::println);
+    }
+
+    public void printFinalResult(List<String> result) {
+        System.out.println(OUTPUT_HEADER_SEPARATOR.getMessage());
+        result.forEach(System.out::println);
+    }
+
+    public void printErrorMessage(String message) {
+        System.out.println(message);
+    }
+}

리뷰: 헉 .. ! 너무나도 좋은 꿀팁 감사드립니다.
@@ -0,0 +1,48 @@
+package store.domain;
+
+import static store.global.constant.MessageConstant.FORMAT;
+
+import java.text.DecimalFormat;
+
+public class PaymentProduct {
+
+    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("###,###");
+
+    private final String name;
+    private final int quantity;
+    private final int price;
+    private final int promotion;
+
+    public PaymentProduct(String name, int quantity, int price, int promotion) {
+        this.name = name;
+        this.quantity = quantity;
+        this.price = price;
+        this.promotion = promotion;
+    }
+
+    @Override
+    public String toString() {
+        String formattedPrice = PRICE_FORMAT.format(quantity * price);
+        return String.format(FORMAT.getMessage(), name, quantity, formattedPrice);
+    }
+
+    public boolean isPromotion() {
+        return promotion != 0;
+    }
+
+    public String buildPromotion() {
+        return String.format(FORMAT.getMessage(), name, promotion, "");
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public int getTotalPrice() {
+        return quantity * price;
+    }
+
+    public int getPromotionPrice() {
+        return promotion * price;
+    }
+}

리뷰: DecimalFormat이라는 클래스는 처음 보네요 ! 배워갑니다
@@ -0,0 +1,48 @@
+package store.domain;
+
+import static store.global.constant.MessageConstant.FORMAT;
+
+import java.text.DecimalFormat;
+
+public class PaymentProduct {
+
+    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("###,###");
+
+    private final String name;
+    private final int quantity;
+    private final int price;
+    private final int promotion;
+
+    public PaymentProduct(String name, int quantity, int price, int promotion) {
+        this.name = name;
+        this.quantity = quantity;
+        this.price = price;
+        this.promotion = promotion;
+    }
+
+    @Override
+    public String toString() {
+        String formattedPrice = PRICE_FORMAT.format(quantity * price);
+        return String.format(FORMAT.getMessage(), name, quantity, formattedPrice);
+    }
+
+    public boolean isPromotion() {
+        return promotion != 0;
+    }
+
+    public String buildPromotion() {
+        return String.format(FORMAT.getMessage(), name, promotion, "");
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public int getTotalPrice() {
+        return quantity * price;
+    }
+
+    public int getPromotionPrice() {
+        return promotion * price;
+    }
+}

리뷰: promotion 변수의 정확한 뜻이 무엇인가요?
프로모션 여부인가요? 프로모션 가격인가요? 굳이 int 타입으로 작성하신 이유가 궁금합니다!
@@ -0,0 +1,89 @@
+package store.domain;
+
+import static store.global.constant.MessageConstant.FORMAT;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PaymentProductList {
+
+    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("###,###");
+
+    private List<PaymentProduct> paymentProducts;
+    private int totalPrice = 0;
+    private int totalPromotion = 0;
+    private int membershipPrice = 0;
+
+    public PaymentProductList() {
+        this.paymentProducts = new ArrayList<>();
+    }
+
+    public void addPaymentProduct(PaymentProduct product) {
+        paymentProducts.add(product);
+    }
+
+    public List<Integer> getAllProductQuantities() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::getQuantity)
+                .toList();
+    }
+
+    public List<String> infoProduct() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::toString)
+                .toList();
+    }
+
+    public List<String> infoPromotion() {
+        return paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .map(PaymentProduct::buildPromotion)
+                .toList();
+    }
+
+    public List<String> finalResult() {
+        List<String> result = new ArrayList<>();
+        result.add(String.format(FORMAT.getMessage(), "총구매액", totalQuantity(), totalPrice()));
+        result.add(String.format(FORMAT.getMessage(), "행사할인", "", totalPromotionPrice()));
+        result.add(String.format(FORMAT.getMessage(), "멤버십할인", "", "-" + PRICE_FORMAT.format(membershipPrice)));
+        result.add(String.format(FORMAT.getMessage(), "내실돈", "",
+                PRICE_FORMAT.format(totalPrice - totalPromotion - membershipPrice)));
+
+        return result;
+    }
+
+    private int totalQuantity() {
+        return paymentProducts.stream()
+                .mapToInt(PaymentProduct::getQuantity)
+                .sum();
+    }
+
+    private String totalPrice() {
+        this.totalPrice = paymentProducts.stream()
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum();
+        return PRICE_FORMAT.format(totalPrice);
+    }
+
+    private String totalPromotionPrice() {
+        this.totalPromotion = paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .mapToInt(PaymentProduct::getPromotionPrice)
+                .sum();
+        return "-" + PRICE_FORMAT.format(totalPromotion);
+    }
+
+    public void applyMembershipDiscount() {
+        int price = (int) (paymentProducts.stream()
+                .filter(product -> !product.isPromotion())
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum() * 0.3);
+
+        if (price > 8000) {
+            price = 8000;
+        }
+
+        this.membershipPrice = price;
+    }
+}

리뷰: 우아한테크코스의 공통 피드백에서 변수명에 자료형(List) 같은 걸 넣지 말라고 했는데, 이에 대해 변수는 아니지만 PaymentProductList 라는 클래스 명에 대해 어떻게 생각하시나요?