ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,34 @@
+package store.config;
+
+import store.controller.StoreController;
+import store.repository.MemoryStoreRepository;
+import store.repository.StoreRepository;
+import store.service.StoreService;
+import store.service.StoreServiceImpl;
+import store.view.InputView;
+import store.view.OutputView;
+
+/** Controller, Service, Repository, View 객체 구현체를 설정하여 객체를 생성하는 클래스 */
+public class AppConfig {
+    
+    private StoreRepository getRepository() {
+        return new MemoryStoreRepository();
+    }
+    
+    private InputView getInputView() {
+        return new InputView();
+    }
+    
+    private OutputView getOutputView() {
+        return new OutputView();
+    }
+    
+    private StoreService getService() {
+        return new StoreServiceImpl(getRepository());
+    }
+    
+    public StoreController getStoreController() {
+        return new StoreController(getInputView(), getOutputView(), getService());
+    }
+    
+}

리뷰: 코드 컨벤션 중 public이 private보다 위에 있어야 한다는 내용이 있습니다.
@@ -0,0 +1,57 @@
+package store.model.discount;
+
+import java.time.LocalDate;
+import java.util.Objects;
+
+public class Promotion {
+    
+    private final String name;
+    
+    private final LocalDate startDate;
+    
+    private final LocalDate endDate;
+    
+    private final int condition;
+    
+    public Promotion(String name, LocalDate startDate,
+                     LocalDate endDate, int condition) {
+        
+        this.name = name;
+        this.startDate = startDate;
+        this.endDate = endDate;
+        this.condition = condition;
+    }
+
+    public int getPromotionCondition() {
+        return this.condition;
+    }
+
+    public LocalDate getStartDate() {
+        return this.startDate;
+    }
+
+    public LocalDate getEndDate() {
+        return endDate;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (this == object) return true;
+        if (object == null || getClass() != object.getClass()) return false;
+        Promotion promotion = (Promotion) object;
+        return condition == promotion.condition && Objects.equals(name, promotion.name)
+                && Objects.equals(startDate, promotion.startDate)
+                && Objects.equals(endDate, promotion.endDate);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, startDate, endDate, condition);
+    }
+
+    @Override
+    public String toString() {
+        return this.name;
+    }
+    
+}

리뷰: 프로모션이 스스로 처리할 수 있는 기능들이 더 있을 것 같습니다. getter만 가지고 있는 점이 조금 아쉽네요.
@@ -1 +1,78 @@
 # java-convenience-store-precourse
+
+## 구현항목
+
+### 입출력
+
+- 영수증
+    - 구매 상품 내역 : 구매한 상품명, 수량, 가격
+    - 증정 상품 내역 : 프로모션에 따라 무료로 제공된 증정 상품의 목록
+    - 금액 정보
+        - 총구매액: 구매한 상품의 총 금액
+        - 총구매수량 : 구매한 상품의 총 수량
+        - 행사할인: 프로모션에 의해 할인된 금액
+        - 멤버십할인: 멤버십에 의해 추가로 할인된 금액(최대 8000원)
+        - 내실돈: 최종 결제 금액(총구매액에서 할인 가격을 뺸 금액)
+
+    * 영수증의 구성 요소를 보기 좋게 정렬
+    * 수량 없을 시 "재고 없음" 출력
+    * 프로모션 상품이 있는 경우 동일한 제품명의 일반 상품 재고가 없으면 일반상품에도 "재고 없음" 출력
+- 상품입력
+    - 상품 입력 형식이 [제품명:갯수] 로 들어왔는지 확인
+
+### 상품
+
+* 상품 promotion이 "null" string으로 들어올 경우 빈 값으로 처리
+
+### 재고관리
+
+* 재고 수량 파악하여 결제 가능 여부 확인
+* 상품 구매 시 재고 차감
+
+### 프로모션(n개 구매 시 1개 무료 증정)
+
+* 오늘 날짜가 프로모션 기간 이내인지 확인
+* 프로모션 재고가 있는지 확인(재고 없을 시 프로모션 적용 불가)
+* 프로모션 재고를 우선적으로 차감
+* 프로모션 적용이 가능한 상품일때 프로모션 조건 수량보다 적게 가져온경우
+  추가 필요수량 안내
+    * Y 입력 시 증정 받을 수 있는 상품 추가
+* 프로모션 재고 부족으로 일부수량을 혜택 없이 결제 시 정가로 결제할지 여부 안내
+    * ex)현재 {상품명} {수량}개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)
+    * N 입력 시 정가로 결제해야하는 수량만큼 제외 후 결제 진행
+* 프로모션 상품의 구매량을 파악하여 안가져온 무료수량을 계산하는 함수 ex) 2+1일때 5개 사면 1개 무료구매 가능
+
+### 멤버십
+
+* 멤버십 회원은 프로모션 미적용 금액의 30% 할인
+* 프로모션 적용 후 프로모션 적용이 되지않은 제품 결제액에 멤버십 할인 적용
+* 멤버십 할인 최대 한도는 8000원
+* 멤버십 할인 여부 물어보고 Y 인 경우에만 멤버십 할인 적용
+
+### 기타
+
+* md파일 읽어오는 기능(상품 목록, 행사 목록)
+* 실패할 경우 실패를 한 해당 질문부터 재시작하는 기능
+* 상품 구매부터 영수증 출력까지(프로그램 1사이클) 수행 후 재구매원할시 제품출력부터 재시작하는 기능
+
+### 예외처리
+
+* 형식 : 에러는 [ERROR]로 시작
+* 상품 입력 형식이 [제품명:갯수] 형식으로 들어오지 않는 경우
+* 여러개일 경우 ,로 구분되어 들어오지 않는 경우
+* 갯수가 자연수값이 아닌 경우
+* 상품명이 중복으로 들어오는 경우
+* 입력한 상품명이 실제 상품에 없는 이름일 경우
+* 갯수가 int 범위 이내가 아닌 경우
+* Y/N으로 입력받는 질문의 경우 Y/N이 아닌 경우
+* 제품의 재고수량보다 구입수량으로 더 큰 값을 입력한 경우
+
+## 프로그램 요구사항
+
+* else 사용 금지
+* enum 적용
+* 단위 테스트 작성
+* 함수 길이 10라인 이하
+* 입출력 별도 클래스 분리 (InputView, OutputView)
+* 현재 날짜와 시간 -> DateTimes.now() 활용
+* 사용자 입력 -> Console.readLine() 활용
\ No newline at end of file

리뷰: 꼼꼼한 예외 처리 정리가 인상적입니다!
README를 잘 정리하신 거 같아요!
@@ -0,0 +1,21 @@
+package util;
+
+import exception.Exception;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ProductValidator {
+
+    private static final String PRODUCT_PATTERN = "^\\[([a-zA-Z가-힣]+)-([1-9]\\d*)\\]$";
+    public static final String INVALID_FORMAT = "올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.";
+
+    public static boolean isValidProductFormat(String input) {
+        Pattern pattern = Pattern.compile(PRODUCT_PATTERN);
+        Matcher matcher = pattern.matcher(input);
+        boolean result = matcher.matches();
+        if (!result) {
+            Exception.throwException(INVALID_FORMAT);
+        }
+        return result;
+    }
+}

리뷰: 올바르지 않은 경우에는 예외를 발생시키는데, 반환값을 boolean으로 설정하신 이유가 궁금합니다!
@@ -0,0 +1,71 @@
+package store;
+
+import view.OutputView;
+
+public class Product {
+
+    private static final String NO_STACK = "재고 없음";
+    private static final String NUMBER = "개";
+    private static final String WON = "원 ";
+
+    private final String name;
+    private int price;
+    private int quantity;
+    private String promotion;
+
+    public Product(String name, int quantity) {
+        this.name = name;
+        this.quantity = quantity;
+    }
+
+    public Product(String name, int price, int quantity) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+    }
+
+    public Product(String name, int price, int quantity, String promotion) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public void print() {
+        String product = "- " + name + " " + String.format("%,d", price) + WON;
+        String count = quantity + NUMBER;
+        if (quantity == 0) {
+            count = NO_STACK;
+        }
+        String promotion = this.promotion;
+        if (promotion == null) {
+            promotion = "";
+        }
+        OutputView.printMessage(product + count + " " + promotion);
+    }
+
+    public void addQuantity(int quantity) {
+        this.quantity += quantity;
+    }
+
+    public void subtractQuantity(int quantity) {
+        this.quantity -= quantity;
+    }
+
+
+    public String getName() {
+        return name;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+}

리뷰: 저도 MVC패턴에 대해서 잘 모르지만, 출력 로직이 모델에 포함되어 있는 거 같아요!
출력 로직은 OutputView에서 관리하시는 게 어떨까요?
@@ -1 +1,78 @@
 # java-convenience-store-precourse
+
+## 구현항목
+
+### 입출력
+
+- 영수증
+    - 구매 상품 내역 : 구매한 상품명, 수량, 가격
+    - 증정 상품 내역 : 프로모션에 따라 무료로 제공된 증정 상품의 목록
+    - 금액 정보
+        - 총구매액: 구매한 상품의 총 금액
+        - 총구매수량 : 구매한 상품의 총 수량
+        - 행사할인: 프로모션에 의해 할인된 금액
+        - 멤버십할인: 멤버십에 의해 추가로 할인된 금액(최대 8000원)
+        - 내실돈: 최종 결제 금액(총구매액에서 할인 가격을 뺸 금액)
+
+    * 영수증의 구성 요소를 보기 좋게 정렬
+    * 수량 없을 시 "재고 없음" 출력
+    * 프로모션 상품이 있는 경우 동일한 제품명의 일반 상품 재고가 없으면 일반상품에도 "재고 없음" 출력
+- 상품입력
+    - 상품 입력 형식이 [제품명:갯수] 로 들어왔는지 확인
+
+### 상품
+
+* 상품 promotion이 "null" string으로 들어올 경우 빈 값으로 처리
+
+### 재고관리
+
+* 재고 수량 파악하여 결제 가능 여부 확인
+* 상품 구매 시 재고 차감
+
+### 프로모션(n개 구매 시 1개 무료 증정)
+
+* 오늘 날짜가 프로모션 기간 이내인지 확인
+* 프로모션 재고가 있는지 확인(재고 없을 시 프로모션 적용 불가)
+* 프로모션 재고를 우선적으로 차감
+* 프로모션 적용이 가능한 상품일때 프로모션 조건 수량보다 적게 가져온경우
+  추가 필요수량 안내
+    * Y 입력 시 증정 받을 수 있는 상품 추가
+* 프로모션 재고 부족으로 일부수량을 혜택 없이 결제 시 정가로 결제할지 여부 안내
+    * ex)현재 {상품명} {수량}개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)
+    * N 입력 시 정가로 결제해야하는 수량만큼 제외 후 결제 진행
+* 프로모션 상품의 구매량을 파악하여 안가져온 무료수량을 계산하는 함수 ex) 2+1일때 5개 사면 1개 무료구매 가능
+
+### 멤버십
+
+* 멤버십 회원은 프로모션 미적용 금액의 30% 할인
+* 프로모션 적용 후 프로모션 적용이 되지않은 제품 결제액에 멤버십 할인 적용
+* 멤버십 할인 최대 한도는 8000원
+* 멤버십 할인 여부 물어보고 Y 인 경우에만 멤버십 할인 적용
+
+### 기타
+
+* md파일 읽어오는 기능(상품 목록, 행사 목록)
+* 실패할 경우 실패를 한 해당 질문부터 재시작하는 기능
+* 상품 구매부터 영수증 출력까지(프로그램 1사이클) 수행 후 재구매원할시 제품출력부터 재시작하는 기능
+
+### 예외처리
+
+* 형식 : 에러는 [ERROR]로 시작
+* 상품 입력 형식이 [제품명:갯수] 형식으로 들어오지 않는 경우
+* 여러개일 경우 ,로 구분되어 들어오지 않는 경우
+* 갯수가 자연수값이 아닌 경우
+* 상품명이 중복으로 들어오는 경우
+* 입력한 상품명이 실제 상품에 없는 이름일 경우
+* 갯수가 int 범위 이내가 아닌 경우
+* Y/N으로 입력받는 질문의 경우 Y/N이 아닌 경우
+* 제품의 재고수량보다 구입수량으로 더 큰 값을 입력한 경우
+
+## 프로그램 요구사항
+
+* else 사용 금지
+* enum 적용
+* 단위 테스트 작성
+* 함수 길이 10라인 이하
+* 입출력 별도 클래스 분리 (InputView, OutputView)
+* 현재 날짜와 시간 -> DateTimes.now() 활용
+* 사용자 입력 -> Console.readLine() 활용
\ No newline at end of file

리뷰: 감사합니다^&^
@@ -0,0 +1,21 @@
+package util;
+
+import exception.Exception;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ProductValidator {
+
+    private static final String PRODUCT_PATTERN = "^\\[([a-zA-Z가-힣]+)-([1-9]\\d*)\\]$";
+    public static final String INVALID_FORMAT = "올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.";
+
+    public static boolean isValidProductFormat(String input) {
+        Pattern pattern = Pattern.compile(PRODUCT_PATTERN);
+        Matcher matcher = pattern.matcher(input);
+        boolean result = matcher.matches();
+        if (!result) {
+            Exception.throwException(INVALID_FORMAT);
+        }
+        return result;
+    }
+}

리뷰: 오 이부분은 사실 예외가 발생하면 return문이 필요없지만
예외가 발생하지 않으면 반환해주는것이 필요할것 같아서 넣어두었습니다.
엄청 예리하시네요...ㅎㅎ
생각을 해보니 반환값을 없애는 방향이 더 좋아보입니다!!
말씀 감사합니다
@@ -0,0 +1,25 @@
+package christmas.config;
+
+public enum Configuration {
+    ;
+
+    private final Object value;
+
+    Configuration(Object value) {
+        this.value = value;
+    }
+
+    public int getIntValue() {
+        if (value instanceof Integer) {
+            return (int) value;
+        }
+        throw new IllegalStateException("Value is not an integer: " + value);
+    }
+
+    public String getStringValue() {
+        if (value instanceof String) {
+            return (String) value;
+        }
+        throw new IllegalStateException("Value is not a string: " + value);
+    }
+}

리뷰: Configuration은 어디서 활용되나요?
@@ -0,0 +1,45 @@
+package christmas.controller;
+
+import christmas.domain.Order;
+import christmas.domain.Orders;
+import christmas.domain.product.Product;
+import christmas.repository.ProductRepository;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+public class InputParser {
+    public static final int ITEM_NAME_INDEX = 0;
+    public static final int ITEM_QUANTITY_INDEX = 1;
+    private final ProductRepository productRepository;
+
+    public InputParser(ProductRepository productRepository) {
+        this.productRepository = productRepository;
+    }
+
+    public int parseInt(String input) {
+        try {
+            return Integer.parseInt(input);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public Orders parseOrders(String input) {
+        List<Order> orders = new ArrayList<>();
+
+        List<String> splitInput = Arrays.stream(input.split(",")).toList();
+        for (String itemInput : splitInput) {
+            List<String> field = Arrays.stream(itemInput.split("-")).toList();
+
+            Optional<Product> product = this.productRepository.findByName(field.get(ITEM_NAME_INDEX).strip());
+            if (product.isEmpty()) {
+                throw new IllegalArgumentException();
+            }
+            orders.add(new Order(product.get(), parseInt(field.get(ITEM_QUANTITY_INDEX).strip())));
+        }
+        return new Orders(orders);
+    }
+
+}

리뷰: Repository를 static이 아닌 의존성 주입으로 사용하신 이유가 궁금합니다!
둘 중 어떤 게 적절한지 고민돼서요
@@ -0,0 +1,56 @@
+package christmas.controller;
+
+import christmas.domain.Orders;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class RetryInputUtil {
+
+    private final InputParser inputParser;
+
+    public RetryInputUtil(InputParser inputParser) {
+        this.inputParser = inputParser;
+    }
+
+    public int getDay() {
+        return retryLogics(InputView::getDay, inputParser::parseInt, this::dayValidate, "유효하지 않은 날짜입니다. 다시 입력해 주세요.");
+    }
+
+    public Orders getOrders() {
+        return retryLogics(InputView::getOrders, inputParser::parseOrders, "유효하지 않은 주문입니다. 다시 입력해 주세요.");
+    }
+
+    private void dayValidate(int day) {
+        if (!(1 <= day && day <= 31)) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    private <T> T retryLogics(Supplier<String> userInputReader, Function<String, T> parser, Consumer<T> validator, String errorMessage) {
+        while (true) {
+            try {
+                String userInput = userInputReader.get();
+                T parsedInput = parser.apply(userInput);
+                validator.accept(parsedInput);
+                return parsedInput;
+            } catch (IllegalArgumentException error) {
+                OutputView.printError(errorMessage);
+            }
+        }
+    }
+
+    private <T> T retryLogics(Supplier<String> userInputReader, Function<String, T> parser, String errorMessage) {
+        while (true) {
+            try {
+                String userInput = userInputReader.get();
+                return parser.apply(userInput);
+            } catch (IllegalArgumentException error) {
+                OutputView.printError(errorMessage);
+            }
+
+        }
+    }
+}

리뷰: Orders에 대한 validation도 추가한다면 retryLogics() 하나로 처리할 수 있을 것 같아요!
@@ -0,0 +1,76 @@
+package christmas.dto;
+
+import christmas.enums.Badge;
+import christmas.enums.BenefitType;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public record BenefitResultDto(
+        int day,
+        List<ItemDto> orderedItems,
+        int totalPrice,
+        Optional<ItemDto> giveaway,
+        Map<BenefitType, Integer> benefits,
+        int totalBenefitAmount,
+        int expectedPaymentAmount,
+        Optional<Badge> badge
+) {
+
+    public static class Builder {
+        int day;
+        List<ItemDto> orderedItems;
+        int totalPrice;
+        Optional<ItemDto> giveaway;
+        Map<BenefitType, Integer> benefits;
+        int totalBenefitAmount;
+        int expectedPaymentAmount;
+        Optional<Badge> badge;
+
+        public Builder day(int day) {
+            this.day = day;
+            return this;
+        }
+
+        public Builder orderedItems(List<ItemDto> orderedItems) {
+            this.orderedItems = orderedItems;
+            return this;
+        }
+
+        public Builder totalPrice(int totalPrice) {
+            this.totalPrice = totalPrice;
+            return this;
+        }
+
+        public Builder giveaway(Optional<ItemDto> giveaway) {
+            this.giveaway = giveaway;
+            return this;
+        }
+
+        public Builder benefits(Map<BenefitType, Integer> benefits) {
+            this.benefits = benefits;
+            return this;
+        }
+
+        public Builder totalBenefitAmount(int totalBenefitAmount) {
+            this.totalBenefitAmount = totalBenefitAmount;
+            return this;
+        }
+
+        public Builder expectedPaymentAmount(int expectedPaymentAmount) {
+            this.expectedPaymentAmount = expectedPaymentAmount;
+            return this;
+        }
+
+        public Builder badge(Optional<Badge> badge) {
+            this.badge = badge;
+            return this;
+        }
+
+        public BenefitResultDto build() {
+            return new BenefitResultDto(day, orderedItems, totalPrice, giveaway, benefits,
+                    totalBenefitAmount,
+                    expectedPaymentAmount, badge);
+        }
+    }
+}

리뷰: 저도 결과 정보들을 dto로 만들었지만 필드가 너무 많은 것이 고민이 되었는데, 빌더를 활용할 수 있겠네요!
@@ -0,0 +1,35 @@
+package christmas.enums;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+public enum Badge {
+    STAR(5_000, "별"),
+    TREE(10_000, "트리"),
+    SANTA(20_000, "산타");
+
+    private final int discountAmount;
+    private final String name;
+
+    Badge(int discountAmount, String name) {
+        this.discountAmount = discountAmount;
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public static Optional<Badge> getBadgeByPaymentAmount(int discountAmount) {
+        List<Badge> badges = Arrays.stream(Badge.values()).sorted().toList();
+
+        Badge selectedBadge = null;
+        for (Badge badge : badges) {
+            if (badge.discountAmount <= discountAmount) {
+                selectedBadge = badge;
+            }
+        }
+        return Optional.ofNullable(selectedBadge);
+    }
+}

리뷰: 여기에 "없음"을 name으로 갖는 enum을 추가하면 Optional<Badge> 대신 활용할 수 있을 것 같습니다!
@@ -0,0 +1,56 @@
+package christmas.controller;
+
+import christmas.domain.Orders;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class RetryInputUtil {
+
+    private final InputParser inputParser;
+
+    public RetryInputUtil(InputParser inputParser) {
+        this.inputParser = inputParser;
+    }
+
+    public int getDay() {
+        return retryLogics(InputView::getDay, inputParser::parseInt, this::dayValidate, "유효하지 않은 날짜입니다. 다시 입력해 주세요.");
+    }
+
+    public Orders getOrders() {
+        return retryLogics(InputView::getOrders, inputParser::parseOrders, "유효하지 않은 주문입니다. 다시 입력해 주세요.");
+    }
+
+    private void dayValidate(int day) {
+        if (!(1 <= day && day <= 31)) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    private <T> T retryLogics(Supplier<String> userInputReader, Function<String, T> parser, Consumer<T> validator, String errorMessage) {
+        while (true) {
+            try {
+                String userInput = userInputReader.get();
+                T parsedInput = parser.apply(userInput);
+                validator.accept(parsedInput);
+                return parsedInput;
+            } catch (IllegalArgumentException error) {
+                OutputView.printError(errorMessage);
+            }
+        }
+    }
+
+    private <T> T retryLogics(Supplier<String> userInputReader, Function<String, T> parser, String errorMessage) {
+        while (true) {
+            try {
+                String userInput = userInputReader.get();
+                return parser.apply(userInput);
+            } catch (IllegalArgumentException error) {
+                OutputView.printError(errorMessage);
+            }
+
+        }
+    }
+}

리뷰: 1과 31같은 매직넘버는 상수로 관리하는 것이 어떨까요?
그리고 조건식의 경우, 긍정 조건식이 가독성이 더 좋다고 합니다!
@@ -0,0 +1,38 @@
+package christmas.domain;
+
+import christmas.dto.ItemDto;
+import christmas.validator.OrdersValidator;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Orders {
+    private final List<Order> orders;
+
+    public Orders(List<Order> orders) {
+        OrdersValidator.validate(orders);
+        this.orders = orders;
+    }
+
+    public int calculateTotalPrice() {
+        return orders.stream().mapToInt(Order::getPrice).sum();
+    }
+
+    public List<Order> getOrders() {
+        return List.copyOf(orders);
+    }
+
+    public List<ItemDto> toDto() {
+        List<ItemDto> items = new ArrayList<>();
+        for (Order order : orders) {
+            items.add(new ItemDto(order.getProduct(), order.getQuantity()));
+        }
+        return items;
+    }
+
+    @Override
+    public String toString() {
+        return "Orders{" +
+               "orders=" + orders +
+               '}';
+    }
+}

리뷰: Stream을 이용해보는것은 어떨까요?
@@ -0,0 +1,36 @@
+package christmas.domain.product;
+
+import christmas.enums.ProductType;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private final ProductType type;
+
+    public Product(String name, int price, ProductType type) {
+        this.name = name;
+        this.price = price;
+        this.type = type;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public ProductType getType() {
+        return type;
+    }
+
+    @Override
+    public String toString() {
+        return "Product{" +
+               "name='" + name + '\'' +
+               ", price=" + price +
+               ", type=" + type +
+               '}';
+    }
+}

리뷰: 사용하지 않는 toString을 재정의한 이유가 있을까요?
@@ -0,0 +1,37 @@
+package christmas.enums;
+
+public enum DayOfWeek {
+    MONDAY(0),
+    TUESDAY(1),
+    WEDNESDAY(2),
+    THURSDAY(3),
+    FRIDAY(4),
+    SATURDAY(5),
+    SUNDAY(6);
+
+    public static final DayOfWeek START_OF_WEEK = FRIDAY;
+    private final int order;
+
+
+    DayOfWeek(int order) {
+        this.order = order;
+    }
+
+    public int getOrder() {
+        return this.order;
+    }
+
+    private static DayOfWeek getDayOfWeekByOrder(int order) {
+        for (DayOfWeek day : DayOfWeek.values()) {
+            if (day.order == order) {
+                return day;
+            }
+        }
+        throw new IllegalArgumentException("Invalid day of week: " + order);
+    }
+
+    public static DayOfWeek getDayOfWeekAsDate(int day) {
+        int order = (START_OF_WEEK.getOrder() + day - 1) / 7;
+        return getDayOfWeekByOrder(order);
+    }
+}

리뷰: 저는 리스트로 모든 날짜를 enum으로 관리했는데, 이렇게 수식으로 처리할 수도 있겠네요!
@@ -0,0 +1,37 @@
+package christmas.config;
+
+import christmas.controller.Controller;
+import christmas.controller.InputParser;
+import christmas.controller.RetryInputUtil;
+import christmas.file.parser.ProductParser;
+import christmas.file.reader.CsvReader;
+import christmas.initializer.ProductInitializer;
+import christmas.repository.ProductRepository;
+import christmas.service.PromotionService;
+
+public class DependencyInjector {
+    public Controller createController() {
+        ProductRepository productRepository = getProductRepository();
+        InputParser inputParser = getInputParser(productRepository);
+        RetryInputUtil retryInputUtil = getRetryInputUtil(inputParser);
+        PromotionService promotionService = new PromotionService(productRepository);
+
+        ProductParser productParser = new ProductParser(CsvReader.of("products.md", false));
+        ProductInitializer initializer = new ProductInitializer(productRepository, productParser);
+        initializer.init();
+
+        return new Controller(retryInputUtil, promotionService);
+    }
+
+    private static ProductRepository getProductRepository() {
+        return new ProductRepository();
+    }
+
+    private static InputParser getInputParser(ProductRepository productRepository) {
+        return new InputParser(productRepository);
+    }
+
+    private static RetryInputUtil getRetryInputUtil(InputParser inputParser) {
+        return new RetryInputUtil(inputParser);
+    }
+}

리뷰: 이번 프리코스 4주차 미션에서 md파일을 읽어오는것이 있었는데 크리스마스 과제도 이렇게 푸시다니 최종 코테 대비를 제대로 하시는군요!
@@ -0,0 +1,23 @@
+package christmas.domain.promotion;
+
+public class GiveawayPromotion {
+
+    private static final int GIVEAWAY_PAYMENT_AMOUNT = 120_000;
+    private static final String GIVEAWAY_PRODUCT_NAME = "샴페인";
+    private static final int GIVEAWAY_PRODUCT_AMOUNT = 1;
+
+    public static boolean isAvailable(int paymentAmount) {
+        if (paymentAmount < GIVEAWAY_PAYMENT_AMOUNT) {
+            return false;
+        }
+        return true;
+    }
+
+    public static String getGiveawayProductName() {
+        return GIVEAWAY_PRODUCT_NAME;
+    }
+
+    public static int getGiveawayProductQuantity() {
+        return GIVEAWAY_PRODUCT_AMOUNT;
+    }
+}

리뷰: 증정품 부분을 하드코딩하는게 아닌, 레파지토리에서 가져오는 로직은 어떠신가요?