ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,27 @@
+package store.exception.messages;
+
+public enum ErrorMessage {
+
+    INVALID_FILE_FORMAT_ERROR("파일 처리 중 오류가 발생했습니다"),
+    INVALID_FORMAT("잘못된 입력 형식입니다. [상품명-수량] 형식으로 입력해 주세요."),
+    INVALID_NUMBER("유효한 숫자를 입력해 주세요."),
+    INVALID_ANSWER("잘못된 입력입니다. Y 또는 N으로 다시 입력해 주세요."),
+    MAXIMUM_NUMBER_LENGTH("숫자는 9자 이내여야합니다."),
+    DUPLICATE_ORDER_ITEM("중복된 상품이 있습니다. 다시 입력해 주세요."),
+    INSUFFICIENT_STOCK("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    NOT_FOUND("존재하지 않는 상품입니다. 다시 입력해 주세요.");
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return "[ERROR] " + message;
+    }
+
+    public String format(Object... args) {
+        return String.format(getMessage(), args);
+    }
+}

리뷰: 공통된 [ERROR]를 분리하신 부분이 되게 좋네요!
@@ -0,0 +1,39 @@
+package store.inventory.parser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import store.inventory.domain.Promotion;
+
+public class PromotionParser {
+
+    private static final int HEADER_LINE = 1;
+    private static final String DELIMITER = ",";
+
+    public static List<Promotion> parsePromotions(String filePath) throws IOException {
+        List<Promotion> promotions = new ArrayList<>();
+
+        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
+            reader.lines()
+                    .skip(HEADER_LINE)
+                    .filter(line -> !line.isBlank())
+                    .forEach(line -> promotions.add(parsePromotion(line)));
+        }
+        return promotions;
+    }
+
+    private static Promotion parsePromotion(String line) {
+        String[] data = line.split(DELIMITER);
+        String name = data[0].trim();
+        int buyQuantity = Integer.parseInt(data[1].trim());
+        int freeQuantity = Integer.parseInt(data[2].trim());
+        LocalDate startDate = LocalDate.parse(data[3].trim());
+        LocalDate endDate = LocalDate.parse(data[4].trim());
+
+        return new Promotion(name, buyQuantity, freeQuantity, startDate, endDate);
+    }
+}
\ No newline at end of file

리뷰: 안녕하세요!

현재 data 배열은 값이 변경되지 않고, 오로지 읽기 전용으로 사용되고 있는데요. 이를 불변 리스트로 관리하면 더 안전하고, 명확하게 표현할 수 있지 않을까 싶어 리뷰 남겨봅니다.

배열 대신 List.of를 사용해 불변 리스트로 만들면, 데이터의 안전성을 보장하면서도 코드 가독성도 개선될 것 같은데 어떻게 생각하시나요? 😊
```
private static Promotion parsePromotion(String line) {
    List<String> data = List.of(line.split(DELIMITER));

    String name = data.get(0).trim();
    int buyQuantity = Integer.parseInt(data.get(1).trim());
    int freeQuantity = Integer.parseInt(data.get(2).trim());
    LocalDate startDate = LocalDate.parse(data.get(3).trim());
    LocalDate endDate = LocalDate.parse(data.get(4).trim());

    return new Promotion(name, buyQuantity, freeQuantity, startDate, endDate);
}
```
@@ -0,0 +1,55 @@
+package store.order.domain;
+
+import static store.order.validator.CartValidator.findInventoryItem;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.stream.Collectors;
+import store.inventory.domain.InventoryItem;
+import store.order.dto.FreeItemDto;
+import store.order.dto.OrderItemDto;
+import store.order.dto.ReceiptItemDto;
+
+public class Cart {
+
+    private final List<CartItem> items;
+
+    public Cart(final List<CartItem> items) {
+        this.items = items;
+    }
+
+    public static Cart of(
+            final List<OrderItemDto> orderItems,
+            final List<InventoryItem> inventoryItems,
+            final LocalDate today) {
+        List<CartItem> cartItems = orderItems.stream().map(orderItemDto -> {
+            final InventoryItem inventoryItem = findInventoryItem(orderItemDto.productName(), inventoryItems);
+            return CartItem.from(orderItemDto, inventoryItem, today);
+        }).collect(Collectors.toList());
+        return new Cart(cartItems);
+    }
+
+    public PaymentCalculator createPaymentCalculator(final boolean membershipStatus) {
+        return new PaymentCalculator(this, membershipStatus);
+    }
+
+    public int calculateTotalPrice() {
+        return items.stream().mapToInt(CartItem::calculateTotalPrice).sum();
+    }
+
+    public int calculatePromotionDiscount() {
+        return items.stream().mapToInt(CartItem::calculatePromotionDiscount).sum();
+    }
+
+    public List<ReceiptItemDto> getPurchasedItems() {
+        return ReceiptItemDto.from(items);
+    }
+
+    public List<FreeItemDto> getFreeItems() {
+        return FreeItemDto.from(items);
+    }
+
+    public List<CartItem> getItems() {
+        return items;
+    }
+}

리뷰: `findInventoryItem` 메서드가 `CartValidator` 클래스에 위치하고 있었군요! 
처음에는 메서드 이름만 보고 `Cart` 클래스에 있는 메서드라고 착각했습니다. 
특임포트 구문을 보고 나서야` CartValidator`에 있는 메서드라는 것을 알 수 있었습니다.

이와 같은 혼란을 줄이기 위해 메서드 호출 시 클래스명을 명시하는 것에 대해 어떻게 생각하시나요?
`final InventoryItem inventoryItem = CartValidator.findInventoryItem(orderItemDto.productName(), inventoryItems);`
@@ -0,0 +1,25 @@
+package store;
+
+import store.controller.StoreController;
+import store.exception.ExceptionHandler;
+import store.inventory.repository.InventoryRepository;
+import store.inventory.repository.PromotionRepository;
+import store.inventory.service.InventoryService;
+import store.inventory.service.PromotionService;
+import store.service.StoreService;
+
+public class Configuration {
+
+    private final InventoryRepository inventoryRepository = new InventoryRepository();
+    private final PromotionRepository promotionRepository = new PromotionRepository();
+    private final PromotionService promotionService = new PromotionService(promotionRepository);
+    private final InventoryService inventoryService = new InventoryService(inventoryRepository, promotionRepository);
+    private final StoreService storeService = new StoreService(inventoryService, promotionService);
+    private final ExceptionHandler exceptionHandler = new ExceptionHandler();
+
+    public StoreController storeController() {
+        StoreController storeController = new StoreController(storeService, exceptionHandler);
+        storeController.init();
+        return storeController;
+    }
+}
\ No newline at end of file

리뷰: storeController.init()을 Configuration 클래스 안에서 호출하신 이유가 있을까요?
@@ -1,7 +1,11 @@
 package store;
 
+import store.controller.StoreController;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        final Configuration configuration = new Configuration();
+        StoreController storeController = configuration.storeController();
+        storeController.run();
     }
 }

리뷰: 메서드 내부에서 final 키워드로 선언하신 이유가 있을까요?
@@ -0,0 +1,73 @@
+package store.inventory.domain;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import store.exception.AdditionalBenefitException;
+import store.exception.PromotionNotAvailableException;
+
+public class PromotionHandler {
+
+    private static final int ZERO = 0;
+
+    private final Stock stock;
+    private Optional<Promotion> promotion;
+
+    public PromotionHandler(final Promotion promotion, final Stock stock) {
+        this.promotion = Optional.ofNullable(promotion);
+        this.stock = stock;
+    }
+
+    public boolean isApplicable(final int quantity, final LocalDate today) {
+        return promotion.isPresent() && promotion.get().isApplicable(quantity, today);
+    }
+
+    public int calculateFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateFreeQuantity(quantity, stock.getPromotionStock())).orElse(0);
+    }
+
+    public void applyDiscount(final int quantity, String productName) {
+        final int promotionQuantity = calculatePromotionStock(quantity);
+        final int nonPromotionQuantity = quantity - promotionQuantity;
+
+        stock.drainDuringPromotionPeriod(promotionQuantity);
+        if (nonPromotionQuantity > ZERO) {
+            throw new PromotionNotAvailableException(productName, nonPromotionQuantity);
+        }
+    }
+
+    public void checkAdditionalBenefitEligibility(final int quantity, final String productName) {
+        final int additionalEligibleQuantity = calculateRemainingFreeQuantity(quantity);
+        if (isEligibleForAdditionalBenefit(additionalEligibleQuantity, quantity)) {
+            throw new AdditionalBenefitException(productName, additionalEligibleQuantity);
+        }
+    }
+
+    public void changePromotion(final Promotion promotion) {
+        this.promotion = Optional.ofNullable(promotion);
+    }
+
+    public String getPromotionName() {
+        return promotion.map(Promotion::getName).orElse("");
+    }
+
+    private int calculateRemainingFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateRemainingFreeQuantity(quantity)).orElse(ZERO);
+    }
+
+    private boolean isEligibleForAdditionalBenefit(final int additionalEligibleQuantity, final int quantity) {
+        final int availablePromotionStock = calculatePromotionStock(quantity);
+        final int totalRequiredStock = calculateTotalRequiredStock(additionalEligibleQuantity, availablePromotionStock);
+
+        return additionalEligibleQuantity > ZERO && stock.getPromotionStock() >= totalRequiredStock;
+    }
+
+    private int calculatePromotionStock(final int quantity) {
+        return promotion.map(promo -> promo.calculateAvailablePromotionStock(quantity, stock.getPromotionStock()))
+                .orElse(ZERO);
+    }
+
+    private int calculateTotalRequiredStock(final int additionalEligibleQuantity, final int availablePromotionStock) {
+        final int promotionSetSize = promotion.get().getPromotionSetSize();
+        return availablePromotionStock + additionalEligibleQuantity * promotionSetSize;
+    }
+}

리뷰: Promotion을 Optional로 관리하신 이유가 있을까요??
@@ -0,0 +1,56 @@
+package store.inventory.domain;
+
+import static store.exception.messages.ErrorMessage.INSUFFICIENT_STOCK;
+
+public class Stock {
+
+    private int generalStock;
+    private int promotionStock;
+
+    public Stock(final int generalStock, final int promotionStock) {
+        this.generalStock = generalStock;
+        this.promotionStock = promotionStock;
+    }
+
+    public void useGeneralStock(final int quantity) {
+        validateGeneralStock(quantity);
+        generalStock -= quantity;
+    }
+
+    public void drainDuringPromotionPeriod(final int totalQuantity) {
+        final int remainingPromo = usePromotionStock(totalQuantity);
+        useGeneralStock(remainingPromo);
+    }
+
+    public void addGeneralStock(final int quantity) {
+        this.generalStock += quantity;
+    }
+
+    public void addPromotionStock(final int quantity) {
+        this.promotionStock += quantity;
+    }
+
+    public int getGeneralStock() {
+        return generalStock;
+    }
+
+    public int getPromotionStock() {
+        return promotionStock;
+    }
+
+    public int getTotalStock() {
+        return generalStock + promotionStock;
+    }
+
+    private int usePromotionStock(final int quantity) {
+        final int usedStock = Math.min(quantity, promotionStock);
+        promotionStock -= usedStock;
+        return quantity - usedStock;
+    }
+
+    private void validateGeneralStock(final int quantity) {
+        if (quantity > generalStock) {
+            throw new IllegalArgumentException(INSUFFICIENT_STOCK.getMessage());
+        }
+    }
+}

리뷰: calculateRemainStock과 같은 네이밍은 어떨까요?
@@ -0,0 +1,73 @@
+package store.inventory.domain;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import store.exception.AdditionalBenefitException;
+import store.exception.PromotionNotAvailableException;
+
+public class PromotionHandler {
+
+    private static final int ZERO = 0;
+
+    private final Stock stock;
+    private Optional<Promotion> promotion;
+
+    public PromotionHandler(final Promotion promotion, final Stock stock) {
+        this.promotion = Optional.ofNullable(promotion);
+        this.stock = stock;
+    }
+
+    public boolean isApplicable(final int quantity, final LocalDate today) {
+        return promotion.isPresent() && promotion.get().isApplicable(quantity, today);
+    }
+
+    public int calculateFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateFreeQuantity(quantity, stock.getPromotionStock())).orElse(0);
+    }
+
+    public void applyDiscount(final int quantity, String productName) {
+        final int promotionQuantity = calculatePromotionStock(quantity);
+        final int nonPromotionQuantity = quantity - promotionQuantity;
+
+        stock.drainDuringPromotionPeriod(promotionQuantity);
+        if (nonPromotionQuantity > ZERO) {
+            throw new PromotionNotAvailableException(productName, nonPromotionQuantity);
+        }
+    }
+
+    public void checkAdditionalBenefitEligibility(final int quantity, final String productName) {
+        final int additionalEligibleQuantity = calculateRemainingFreeQuantity(quantity);
+        if (isEligibleForAdditionalBenefit(additionalEligibleQuantity, quantity)) {
+            throw new AdditionalBenefitException(productName, additionalEligibleQuantity);
+        }
+    }
+
+    public void changePromotion(final Promotion promotion) {
+        this.promotion = Optional.ofNullable(promotion);
+    }
+
+    public String getPromotionName() {
+        return promotion.map(Promotion::getName).orElse("");
+    }
+
+    private int calculateRemainingFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateRemainingFreeQuantity(quantity)).orElse(ZERO);
+    }
+
+    private boolean isEligibleForAdditionalBenefit(final int additionalEligibleQuantity, final int quantity) {
+        final int availablePromotionStock = calculatePromotionStock(quantity);
+        final int totalRequiredStock = calculateTotalRequiredStock(additionalEligibleQuantity, availablePromotionStock);
+
+        return additionalEligibleQuantity > ZERO && stock.getPromotionStock() >= totalRequiredStock;
+    }
+
+    private int calculatePromotionStock(final int quantity) {
+        return promotion.map(promo -> promo.calculateAvailablePromotionStock(quantity, stock.getPromotionStock()))
+                .orElse(ZERO);
+    }
+
+    private int calculateTotalRequiredStock(final int additionalEligibleQuantity, final int availablePromotionStock) {
+        final int promotionSetSize = promotion.get().getPromotionSetSize();
+        return availablePromotionStock + additionalEligibleQuantity * promotionSetSize;
+    }
+}

리뷰: Promotion 클래스에서도 해당 상수가 선언되어있는데, 따로 클래스로 분리해 재사용성을 고려하는건 어떨까요?
@@ -0,0 +1,25 @@
+package store.inventory.repository;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import store.inventory.domain.InventoryItem;
+
+public class InventoryRepository {
+
+    private final List<InventoryItem> inventories = new ArrayList<>();
+
+    public void save(InventoryItem inventoryItem) {
+        inventories.add(inventoryItem);
+    }
+
+    public List<InventoryItem> findAll() {
+        return new ArrayList<>(inventories);
+    }
+
+    public Optional<InventoryItem> findByProductName(String productName) {
+        return inventories.stream()
+                .filter(inventory -> inventory.getProductName().equals(productName))
+                .findFirst();
+    }
+}

리뷰: repository를 구현할 생각은 못했는데, 정말 좋은 것 같아요!!👍
@@ -0,0 +1,17 @@
+package store.exception;
+
+import static store.exception.messages.UserPromotionMessage.ADDITIONAL_BENEFIT_AVAILABLE;
+
+public class AdditionalBenefitException extends RuntimeException {
+
+    private final int additionalEligibleQuantity;
+
+    public AdditionalBenefitException(String productName, int additionalEligibleQuantity) {
+        super(ADDITIONAL_BENEFIT_AVAILABLE.format(productName, additionalEligibleQuantity));
+        this.additionalEligibleQuantity = additionalEligibleQuantity;
+    }
+
+    public int getAdditionalEligibleQuantity() {
+        return additionalEligibleQuantity;
+    }
+}

리뷰: 저는 이번에 시간이 없어서 커스텀 예외 클래스를 생각만 하고 만들지 못했는데,, 대단하신거 같습니다!
@@ -0,0 +1,25 @@
+package store.inventory.repository;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import store.inventory.domain.InventoryItem;
+
+public class InventoryRepository {
+
+    private final List<InventoryItem> inventories = new ArrayList<>();
+
+    public void save(InventoryItem inventoryItem) {
+        inventories.add(inventoryItem);
+    }
+
+    public List<InventoryItem> findAll() {
+        return new ArrayList<>(inventories);
+    }
+
+    public Optional<InventoryItem> findByProductName(String productName) {
+        return inventories.stream()
+                .filter(inventory -> inventory.getProductName().equals(productName))
+                .findFirst();
+    }
+}

리뷰: JPA처럼 Optional로 반환하는것 잘 배우고 갑니다!
@@ -0,0 +1,39 @@
+package store.inventory.parser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import store.inventory.domain.Promotion;
+
+public class PromotionParser {
+
+    private static final int HEADER_LINE = 1;
+    private static final String DELIMITER = ",";
+
+    public static List<Promotion> parsePromotions(String filePath) throws IOException {
+        List<Promotion> promotions = new ArrayList<>();
+
+        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
+            reader.lines()
+                    .skip(HEADER_LINE)
+                    .filter(line -> !line.isBlank())
+                    .forEach(line -> promotions.add(parsePromotion(line)));
+        }
+        return promotions;
+    }
+
+    private static Promotion parsePromotion(String line) {
+        String[] data = line.split(DELIMITER);
+        String name = data[0].trim();
+        int buyQuantity = Integer.parseInt(data[1].trim());
+        int freeQuantity = Integer.parseInt(data[2].trim());
+        LocalDate startDate = LocalDate.parse(data[3].trim());
+        LocalDate endDate = LocalDate.parse(data[4].trim());
+
+        return new Promotion(name, buyQuantity, freeQuantity, startDate, endDate);
+    }
+}
\ No newline at end of file

리뷰: 좋은 의견인 것 같습니다. 자세한 리뷰 및 설명 정말 감사합니다!
@@ -0,0 +1,55 @@
+package store.order.domain;
+
+import static store.order.validator.CartValidator.findInventoryItem;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.stream.Collectors;
+import store.inventory.domain.InventoryItem;
+import store.order.dto.FreeItemDto;
+import store.order.dto.OrderItemDto;
+import store.order.dto.ReceiptItemDto;
+
+public class Cart {
+
+    private final List<CartItem> items;
+
+    public Cart(final List<CartItem> items) {
+        this.items = items;
+    }
+
+    public static Cart of(
+            final List<OrderItemDto> orderItems,
+            final List<InventoryItem> inventoryItems,
+            final LocalDate today) {
+        List<CartItem> cartItems = orderItems.stream().map(orderItemDto -> {
+            final InventoryItem inventoryItem = findInventoryItem(orderItemDto.productName(), inventoryItems);
+            return CartItem.from(orderItemDto, inventoryItem, today);
+        }).collect(Collectors.toList());
+        return new Cart(cartItems);
+    }
+
+    public PaymentCalculator createPaymentCalculator(final boolean membershipStatus) {
+        return new PaymentCalculator(this, membershipStatus);
+    }
+
+    public int calculateTotalPrice() {
+        return items.stream().mapToInt(CartItem::calculateTotalPrice).sum();
+    }
+
+    public int calculatePromotionDiscount() {
+        return items.stream().mapToInt(CartItem::calculatePromotionDiscount).sum();
+    }
+
+    public List<ReceiptItemDto> getPurchasedItems() {
+        return ReceiptItemDto.from(items);
+    }
+
+    public List<FreeItemDto> getFreeItems() {
+        return FreeItemDto.from(items);
+    }
+
+    public List<CartItem> getItems() {
+        return items;
+    }
+}

리뷰: 코드의 간결성을 위해 생략했었는데 오히려 다른 사람 입장에서 가독성이 낮아질 수도 있겠구나 깨닫게된 리뷰인 것 같습니다. 이후의 협업을 위해 이러한 사항을 고려하는 더 섬세한 개발자가 되어야겠습니다. 감사합니다!
@@ -0,0 +1,25 @@
+package store;
+
+import store.controller.StoreController;
+import store.exception.ExceptionHandler;
+import store.inventory.repository.InventoryRepository;
+import store.inventory.repository.PromotionRepository;
+import store.inventory.service.InventoryService;
+import store.inventory.service.PromotionService;
+import store.service.StoreService;
+
+public class Configuration {
+
+    private final InventoryRepository inventoryRepository = new InventoryRepository();
+    private final PromotionRepository promotionRepository = new PromotionRepository();
+    private final PromotionService promotionService = new PromotionService(promotionRepository);
+    private final InventoryService inventoryService = new InventoryService(inventoryRepository, promotionRepository);
+    private final StoreService storeService = new StoreService(inventoryService, promotionService);
+    private final ExceptionHandler exceptionHandler = new ExceptionHandler();
+
+    public StoreController storeController() {
+        StoreController storeController = new StoreController(storeService, exceptionHandler);
+        storeController.init();
+        return storeController;
+    }
+}
\ No newline at end of file

리뷰: 파일 일관성 상 컨피그 파일에서 프로그램 설정을 완료해야한다고 생각했습니다. 지금 생각해보니 컨트롤러 내의 init을 컨피그 파일로 이전하지 않는 이상 컨트롤러 start() 메서드 내에서 호출해도 됐을 것 같네요!
@@ -1,7 +1,11 @@
 package store;
 
+import store.controller.StoreController;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        final Configuration configuration = new Configuration();
+        StoreController storeController = configuration.storeController();
+        storeController.run();
     }
 }

리뷰: 최대한 피드백 대로 final을 사용하고 싶었습니다. final을 적용하면서 어디부터 어디까지 적용해야하나라는 의문이 생겼었는데 그때 시간이 부족하여 일단 웬만하면 적용하는 식으로 했습니다. 따라서 현재 과제에서는 final 적용에 대해 많이 서툰 것 같네요 매개변수, 정말 바뀌지 않는 곳에서만 final 키워드를 사용할 수 있도록 해야겠습니다!