ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,68 @@
+package christmas.model;
+
+import christmas.util.Constant;
+import christmas.util.ErrorMessage;
+import christmas.util.Utility;
+import christmas.util.Validator;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.List;
+
+public class OrderMenu {
+    private final List<Map<String, Integer>> menu;
+
+    private int orderDate;
+    private Map<String, Integer> userOrder;
+
+    public OrderMenu() {
+        menu = new ArrayList<>(List.of(
+                // EPPETIZER 0
+                Map.of("양송이수프", 6000, "타파스", 5500, "시저샐러드", 8000),
+                // MAINMENU 1
+                Map.of("티본스테이크", 55000, "바비큐립", 54000, "해산물파스타", 35000, "크리스마스파스타", 25000),
+                // DESSERT 2
+                Map.of("초코케이크", 15000, "아이스크림", 5000),
+                // BEVERAGE 3
+                Map.of("제로콜라", 3000, "레드와인", 60000, "샴페인", 25000)
+        ));
+    }
+
+    public int getOrderDate() {
+        return this.orderDate;
+    }
+
+    public Map<String, Integer> getUserOrder() {
+        return this.userOrder;
+    }
+
+    public int getTotalPrice() {
+        int totalPrice = Constant.ZERO.get();
+        for (Map.Entry<String, Integer> entry : this.userOrder.entrySet()) {
+            int category = Utility.getCategory(entry.getKey(), menu);
+            int price = menu.get(category).get(entry.getKey());
+            totalPrice += price * entry.getValue();
+        }
+        return totalPrice;
+    }
+
+    public int[] getCategoryCount() {
+        int[] categoryCount = new int[Constant.CATEGORY_COUNT.get()];
+        for (Map.Entry<String, Integer> entry : this.userOrder.entrySet()) {
+            int category = Utility.getCategory(entry.getKey(), menu);
+            categoryCount[category] += entry.getValue();
+        }
+        return categoryCount;
+    }
+
+    public void setOrderDate(int date) {
+        this.orderDate = date;
+    }
+
+    public void setUserOrder(Map<String, Integer> userOrder) {
+        if (Validator.validateMenu(userOrder, this.menu)) {
+            throw new IllegalArgumentException(ErrorMessage.ORDER_ERROR.get());
+        }
+        this.userOrder = userOrder;
+    }
+}

리뷰: 이 점은 제가 아예 몰랐던 부분이었습니다. 하드코딩하면서도 좋은 방식이 아니라고 느꼈는데 enum을 사용하면 됐었네요...
@@ -0,0 +1,39 @@
+package christmas.util;
+
+public enum Constant {
+    NOT_IN_MENU(-1),
+    EPPETIZER(0),
+    MAINMENU(1),
+    DESSERT(2),
+    BEVERAGE(3),
+    CATEGORY_COUNT(4),
+    ZERO(0),
+    ONE(1),
+    THOUSAND(1000),
+    HUNDRED(100),
+    CHRISTMAS_DAY(25),
+    MIN_BENEFIT_PRICE(10000),
+    PRESENT_COST(120000),
+    CHAMPAIGN_PRICE(25000),
+    WEEKDAYS(7),
+    FRIDAY(1),
+    SATURDAY(2),
+    SUNDAY(3),
+    EVENT_PRICE(2023),
+    SANTA(20000),
+    TREE(10000),
+    STAR(5000),
+    MAX_MENU_COUNT(20),
+    KEY_INDEX(0),
+    VALUE_INDEX(1);
+
+    private final int value;
+
+    Constant(int value) {
+        this.value = value;
+    }
+
+    public int get() {
+        return this.value;
+    }
+}

리뷰: 상수를 처리한다는 것에만 신경쓰다 보니 미처 생각을 못했습니다. 다시 확인해보니 가독성 측면에서도 많이 불편한 것 같아요. 감사합니다!
@@ -0,0 +1,68 @@
+package christmas.model;
+
+import christmas.util.Constant;
+import christmas.util.ErrorMessage;
+import christmas.util.Utility;
+import christmas.util.Validator;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.List;
+
+public class OrderMenu {
+    private final List<Map<String, Integer>> menu;
+
+    private int orderDate;
+    private Map<String, Integer> userOrder;
+
+    public OrderMenu() {
+        menu = new ArrayList<>(List.of(
+                // EPPETIZER 0
+                Map.of("양송이수프", 6000, "타파스", 5500, "시저샐러드", 8000),
+                // MAINMENU 1
+                Map.of("티본스테이크", 55000, "바비큐립", 54000, "해산물파스타", 35000, "크리스마스파스타", 25000),
+                // DESSERT 2
+                Map.of("초코케이크", 15000, "아이스크림", 5000),
+                // BEVERAGE 3
+                Map.of("제로콜라", 3000, "레드와인", 60000, "샴페인", 25000)
+        ));
+    }
+
+    public int getOrderDate() {
+        return this.orderDate;
+    }
+
+    public Map<String, Integer> getUserOrder() {
+        return this.userOrder;
+    }
+
+    public int getTotalPrice() {
+        int totalPrice = Constant.ZERO.get();
+        for (Map.Entry<String, Integer> entry : this.userOrder.entrySet()) {
+            int category = Utility.getCategory(entry.getKey(), menu);
+            int price = menu.get(category).get(entry.getKey());
+            totalPrice += price * entry.getValue();
+        }
+        return totalPrice;
+    }
+
+    public int[] getCategoryCount() {
+        int[] categoryCount = new int[Constant.CATEGORY_COUNT.get()];
+        for (Map.Entry<String, Integer> entry : this.userOrder.entrySet()) {
+            int category = Utility.getCategory(entry.getKey(), menu);
+            categoryCount[category] += entry.getValue();
+        }
+        return categoryCount;
+    }
+
+    public void setOrderDate(int date) {
+        this.orderDate = date;
+    }
+
+    public void setUserOrder(Map<String, Integer> userOrder) {
+        if (Validator.validateMenu(userOrder, this.menu)) {
+            throw new IllegalArgumentException(ErrorMessage.ORDER_ERROR.get());
+        }
+        this.userOrder = userOrder;
+    }
+}

리뷰: 저도 이 부분을 처리하면서 좋은 방식이 아니라고 느꼈는데, enum을 통해 처리할 수 있단 생각은 못했네요. 해운님 코드를 보고 enum을 잘 처리하셨다고 느꼈는데 참고해서 적용해봐야겠습니다. 감사합니다!
@@ -0,0 +1,68 @@
+package christmas.model;
+
+import christmas.util.Constant;
+import christmas.util.ErrorMessage;
+import christmas.util.Utility;
+import christmas.util.Validator;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.List;
+
+public class OrderMenu {
+    private final List<Map<String, Integer>> menu;
+
+    private int orderDate;
+    private Map<String, Integer> userOrder;
+
+    public OrderMenu() {
+        menu = new ArrayList<>(List.of(
+                // EPPETIZER 0
+                Map.of("양송이수프", 6000, "타파스", 5500, "시저샐러드", 8000),
+                // MAINMENU 1
+                Map.of("티본스테이크", 55000, "바비큐립", 54000, "해산물파스타", 35000, "크리스마스파스타", 25000),
+                // DESSERT 2
+                Map.of("초코케이크", 15000, "아이스크림", 5000),
+                // BEVERAGE 3
+                Map.of("제로콜라", 3000, "레드와인", 60000, "샴페인", 25000)
+        ));
+    }
+
+    public int getOrderDate() {
+        return this.orderDate;
+    }
+
+    public Map<String, Integer> getUserOrder() {
+        return this.userOrder;
+    }
+
+    public int getTotalPrice() {
+        int totalPrice = Constant.ZERO.get();
+        for (Map.Entry<String, Integer> entry : this.userOrder.entrySet()) {
+            int category = Utility.getCategory(entry.getKey(), menu);
+            int price = menu.get(category).get(entry.getKey());
+            totalPrice += price * entry.getValue();
+        }
+        return totalPrice;
+    }
+
+    public int[] getCategoryCount() {
+        int[] categoryCount = new int[Constant.CATEGORY_COUNT.get()];
+        for (Map.Entry<String, Integer> entry : this.userOrder.entrySet()) {
+            int category = Utility.getCategory(entry.getKey(), menu);
+            categoryCount[category] += entry.getValue();
+        }
+        return categoryCount;
+    }
+
+    public void setOrderDate(int date) {
+        this.orderDate = date;
+    }
+
+    public void setUserOrder(Map<String, Integer> userOrder) {
+        if (Validator.validateMenu(userOrder, this.menu)) {
+            throw new IllegalArgumentException(ErrorMessage.ORDER_ERROR.get());
+        }
+        this.userOrder = userOrder;
+    }
+}

리뷰: 말씀해주신 부분 다시 보니까 클래스 이름이 내용을 다 포괄하지 못해 어색하게 느껴지네요. 주문한 부분을 다 처리하기 위해 날짜가 필요한데 클래스의 이름을 변경해보면 될 것 같습니다 감사합니다!
@@ -0,0 +1,47 @@
+package christmas.controller;
+
+import java.util.Map;
+
+import christmas.view.*;
+import christmas.model.*;
+
+public class MainController {
+    private OrderMenu orderMenu;
+
+    public MainController() {
+        orderMenu = new OrderMenu();
+    }
+
+    public void run() {
+        orderMenu.setOrderDate(InputView.readDate());
+        setReadOrder();
+
+        OutputView.printPreview(orderMenu.getOrderDate());
+        OutputView.printOrder(orderMenu.getUserOrder());
+        int totalPrice = orderMenu.getTotalPrice();
+        OutputView.printTotalPrice(totalPrice);
+        OutputView.printPresent(totalPrice);
+        benefitControl(totalPrice);
+    }
+
+    private void benefitControl(int totalPrice) {
+        Benefit benefit = new Benefit(orderMenu.getOrderDate(), orderMenu.getCategoryCount(), totalPrice);
+        OutputView.printBenefits(benefit.getBenefits());
+
+        int totalBenefitPrice = benefit.getTotalBenefitPrice();
+        OutputView.printTotalBenefitPrice(totalBenefitPrice);
+        OutputView.printTotalPayment(totalPrice - totalBenefitPrice + benefit.getPresentPrice());
+        OutputView.printEventBadge(totalBenefitPrice);
+    }
+
+    private void setReadOrder() {
+        Map<String, Integer> orderMenu;
+        try {
+            orderMenu = InputView.readOrder();
+            this.orderMenu.setUserOrder(orderMenu);
+        } catch (IllegalArgumentException e) {
+            System.out.println(e.getMessage());
+            setReadOrder();
+        }
+    }
+}

리뷰: MVC 각각은 분리되어 있는 요소다 보니까 각각을 분리된 객체로 보고 인스턴스화해서 사용하는 것 같아요. 물론 이게 정답이라는 건 아닙니다. 말씀하신대로 상태를 가지지 않으니 스태틱 메서드로 만드신 것도 한 가지 방법인 것 같습니다.

참고로, 스태틱 메서드만으로 구성된 객체를 사용할 경우 문제를 찾아봤는데, 많은 경우 뷰에서 상태를 가져야 하는 경우가 많으며, 스태틱 메서드의 경우 테스트 시 목 객체를 만들기 어렵고 상속이나 다형성을 사용하기 어려워서 확장성이나 유연성이 떨어진다고 하네요.
@@ -0,0 +1,42 @@
+import Customer from '../src/models/Customer';
+import Order from '../src/models/Order';
+
+describe('Customer 클래스', () => {
+  let customer;
+
+  beforeEach(() => {
+    customer = new Customer();
+  });
+
+  test('주문이 없을 때, 뱃지 계산 결과는 null이 된다.', () => {
+    customer.calculateBadge();
+    expect(customer.getBadge()).toBeNull();
+  });
+
+  test('적절한 할인 금액에 따라 올바른 뱃지가 할당된다.', () => {
+    const order = new Order();
+    order.setDiscountAmount(10000);
+    customer.addOrder(order);
+    customer.calculateBadge();
+    expect(customer.getBadge()).toBe('트리');
+  });
+
+  test('여러 주문을 추가했을 때, 총 할인 금액에 따른 뱃지가 할당된다.', () => {
+    const order1 = new Order();
+    order1.setDiscountAmount(3000);
+    const order2 = new Order();
+    order2.setDiscountAmount(2000);
+    customer.addOrder(order1);
+    customer.addOrder(order2);
+    customer.calculateBadge();
+    expect(customer.getBadge()).toBe('별');
+  });
+
+  test('할인 금액이 5000원 이하일 때 뱃지는 null로 표시된다.', () => {
+    const order = new Order();
+    order.setDiscountAmount(4000);
+    customer.addOrder(order);
+    customer.calculateBadge();
+    expect(customer.getBadge()).toBeNull();
+  });
+});

리뷰: 지금은 `트리`에 대한 테스트 케이스만 있는데, `별`과 `산타` 테스트 케이스까지 모두 포함하면 테스트 신뢰도가 높아질 것 같아요!
@@ -0,0 +1,48 @@
+import Menu from '../src/models/Menu';
+import { MenuConstants } from '../src/constants/MenuConstants';
+
+describe('Menu 클래스', () => {
+  let menu;
+
+  beforeEach(() => {
+    menu = new Menu();
+  });
+
+  test('메뉴 항목이 올바르게 초기화되어야 한다.', () => {
+    expect(menu.getItem('티본스테이크')).toEqual({
+      name: '티본스테이크',
+      price: 55000,
+      category: 'MAINS',
+    });
+    expect(menu.getItem('시저샐러드')).toEqual({
+      name: '시저샐러드',
+      price: 8000,
+      category: 'APPETIZERS',
+    });
+  });
+
+  test('존재하지 않는 메뉴 항목에 대해 null을 반환해야 한다.', () => {
+    expect(menu.getItem('양송이스프')).toBeNull();
+    expect(menu.getItem('1234')).toBeNull();
+    expect(menu.getItem('스테이크@')).toBeNull();
+  });
+
+  test('모든 메뉴 카테고리가 올바르게 로드되어야 한다.', () => {
+    Object.keys(MenuConstants).forEach((category) => {
+      Object.keys(MenuConstants[category]).forEach((itemName) => {
+        const menuItem = menu.getItem(itemName);
+        expect(menuItem).not.toBeNull();
+        expect(menuItem.category).toBe(category);
+      });
+    });
+  });
+
+  test('각 메뉴 항목의 가격이 정확해야 함', () => {
+    Object.keys(MenuConstants).forEach((category) => {
+      Object.entries(MenuConstants[category]).forEach(([itemName, price]) => {
+        const menuItem = menu.getItem(itemName);
+        expect(menuItem.price).toBe(price);
+      });
+    });
+  });
+});

리뷰: 여기 두 테스트 모두 여러 개의 `expect()`를 가지고 있는데요. `expect()`하는 부분의 구조가 비슷하니까 "메뉴 항목 초기화 테스트"와 "존재하지 않는 메뉴 항목 테스트"를 `test.each()`로 변경하는 건 어떨까요?. 프로덕션 코드를 작성할 때 중복되는 부분을 제거하는 것처럼, 테스트 코드도 마찬가지로 중복을 제거하는 리팩터링이 필요하다고 생각합니다!
@@ -0,0 +1,73 @@
+import OrderValidator from '../src/validators/OrderValidator';
+import Menu from '../src/models/Menu';
+import { ValidatorConstants } from '../src/validators/constants/ValidatorConstants';
+
+describe('OrderValidator 클래스', () => {
+  let mockMenu;
+
+  beforeEach(() => {
+    mockMenu = new Menu();
+  });
+
+  describe('validateOrderItems 메서드', () => {
+    test('유효한 주문 항목에 대해 예외가 발생하지 않아야 한다', () => {
+      const orderItems = [{ name: '티본스테이크', quantity: 1 }];
+      expect(() => OrderValidator.validateOrderItems(orderItems, mockMenu)).not.toThrow();
+    });
+
+    test('존재하지 않는 메뉴 항목에 대해 예외가 발생해야 한다', () => {
+      const orderItems = [{ name: '미지의메뉴', quantity: 1 }];
+      expect(() => OrderValidator.validateOrderItems(orderItems, mockMenu)).toThrow(
+        new Error(ValidatorConstants.INVALID_ORDER_ERROR),
+      );
+    });
+
+    test('음료만 포함된 주문 시 예외가 발생해야 한다', () => {
+      const orderItems = [{ name: '제로콜라', quantity: 2 }];
+      expect(() => OrderValidator.validateOrderItems(orderItems, mockMenu)).toThrow(
+        new Error(ValidatorConstants.ONLY_BEVERAGE_ERROR),
+      );
+    });
+
+    test('주문 항목의 총 수량이 20개를 초과하는 경우 예외가 발생해야 한다', () => {
+      const orderItems = new Array(21).fill({ name: '티본스테이크', quantity: 1 });
+      expect(() => OrderValidator.validateOrderItems(orderItems, mockMenu)).toThrow(
+        new Error(ValidatorConstants.INVALID_ORDER_ERROR),
+      );
+    });
+    test('주문 항목의 총 수량이 20개 이하인 경우 예외가 발생하지 않아야 한다', () => {
+      const orderItems = new Array(20).fill({ name: '티본스테이크', quantity: 1 });
+      expect(() => OrderValidator.validateTotalQuantity(orderItems)).not.toThrow();
+    });
+
+    test('주문 수량이 0 이하인 경우 예외가 발생해야 한다', () => {
+      const orderItems = [{ name: '티본스테이크', quantity: 0 }];
+      expect(() => OrderValidator.validateOrderItems(orderItems, mockMenu)).toThrow(
+        new Error(ValidatorConstants.INVALID_ORDER_ERROR),
+      );
+    });
+
+    test('수량이 1 이상인 경우 예외가 발생하지 않아야 한다', () => {
+      const item = { name: '티본스테이크', quantity: 1 };
+      expect(() => OrderValidator.validateQuantity(item)).not.toThrow();
+    });
+
+    test('중복된 메뉴 항목이 포함된 경우 예외가 발생해야 한다', () => {
+      const orderItems = [
+        { name: '티본스테이크', quantity: 1 },
+        { name: '티본스테이크', quantity: 1 },
+      ];
+      expect(() => OrderValidator.validateOrderItems(orderItems, mockMenu)).toThrow(
+        new Error(ValidatorConstants.INVALID_ORDER_ERROR),
+      );
+    });
+
+    test('음식과 음료가 혼합된 유효한 주문은 예외가 발생하지 않아야 한다', () => {
+      const orderItems = [
+        { name: '티본스테이크', quantity: 1 },
+        { name: '제로콜라', quantity: 2 },
+      ];
+      expect(() => OrderValidator.validateOrderItems(orderItems, mockMenu)).not.toThrow();
+    });
+  });
+});

리뷰: `Validator` 테스트 파일에서는 예외가 발생했을 때 에러의 유형과 메시지 포함 여부를 검사하고 있는데요. 다른 곳의 예외 테스트에서는 메시지 검사 없이 예외가 발생하는지만(`toThrow()`) 검사하는 이유가 궁금합니다.
@@ -0,0 +1,56 @@
+import InputView from '../views/InputView';
+import OutputView from '../views/OutputView';
+import OrderService from '../services/OrderService';
+import DiscountService from '../services/DiscountService';
+import Customer from '../models/Customer';
+import { DateFormatter } from '../utils/DateFormatter';
+
+export default class EventController {
+  #orderService;
+
+  #customer;
+
+  #discountService;
+
+  constructor() {
+    this.#orderService = new OrderService();
+    this.#customer = new Customer();
+    this.#discountService = new DiscountService();
+  }
+
+  async start() {
+    OutputView.printWelcomeMessage();
+    await this.handleOrderProcess();
+  }
+
+  async handleOrderProcess() {
+    try {
+      const inputDate = await this.getDateFromUser();
+      await this.handleMenuOrderProcess(inputDate);
+    } catch (error) {
+      OutputView.printErrorMessage(error.message);
+      await this.handleOrderProcess();
+    }
+  }
+
+  async getDateFromUser() {
+    const day = await InputView.readDate();
+    return DateFormatter.createDateFromDay(day);
+  }
+
+  async handleMenuOrderProcess(inputDate) {
+    try {
+      const inputMenuItems = await InputView.readMenu();
+      const order = this.#orderService.createOrder(inputDate, inputMenuItems);
+      order.applyDiscounts(this.#discountService, inputDate);
+      this.#customer.addOrder(order);
+      this.#customer.calculateBadge();
+      OutputView.printDay(inputDate);
+      OutputView.printOrderDetails(order);
+      OutputView.printBadge(this.#customer);
+    } catch (error) {
+      OutputView.printErrorMessage(error.message);
+      await this.handleMenuOrderProcess(inputDate);
+    }
+  }
+}

리뷰: 함수 라인 수가 딱 15라인인 것 같네요. 만약 어떤 출력이 하나 추가된다면 여기에 추가해야 할텐데, 그러면 함수 라인 수가 15라인을 넘길 수도 있을 것 같아요.

함수 라인 수 제한이 있는 것은 15라인이라는 게 중요해서라기보다 그만큼 **함수를 잘 분리하자** 는 의미인 것 같습니다. 그래서 함수의 라인 수가 간당간당하다는 것은, 리팩터링의 신호가 아닐까 싶어요🤔

이 메서드는 동시에 여러 가지 일을 처리하고 있는 것처럼 보이는데요(입력 받기, 주문 생성, 각종 할인 적용, 출력, 입력 예외 처리 및 재입력 받기 등). 메서드가 하나의 역할만 수행할 수 있도록 분리하는 것은 어떨까요?
@@ -0,0 +1,30 @@
+export default class Customer {
+  #orders;
+
+  #badge;
+
+  static BADGE_CRITERIA = [
+    { name: '산타', amount: 20000 },
+    { name: '트리', amount: 10000 },
+    { name: '별', amount: 5000 },
+  ];
+
+  constructor() {
+    this.#orders = [];
+    this.#badge = null;
+  }
+
+  addOrder(order) {
+    this.#orders.push(order);
+  }
+
+  calculateBadge() {
+    const totalDiscount = this.#orders.reduce((sum, order) => sum + order.getDiscountAmount(), 0);
+    const badge = Customer.BADGE_CRITERIA.find((badgeName) => totalDiscount >= badgeName.amount);
+    this.#badge = badge ? badge.name : null;
+  }
+
+  getBadge() {
+    return this.#badge;
+  }
+}

리뷰: 상수를 파일로 만든다면 수정이 필요할 때 찾기가 수월하고, 배지 정보가 늘어나거나 줄어들어도 관리하기 편리할 것 같은데요. 파일로 만들지 않고 여기에 작성하신 이유가 있나요?
@@ -0,0 +1,81 @@
+import { OrderConstants } from './constants/OrderConstants';
+
+export default class Order {
+  #menuItems;
+
+  #totalAmount;
+
+  #discountAmount;
+
+  #gift;
+
+  #discountDetails;
+
+  constructor() {
+    this.#menuItems = new Map();
+    this.#totalAmount = 0;
+    this.#discountAmount = 0;
+    this.#gift = null;
+    this.#discountDetails = {};
+  }
+
+  applyDiscounts(discountService, date) {
+    const discountResults = discountService.applyDiscounts(this, date);
+    this.setDiscountDetails(discountResults);
+    this.checkForGift();
+  }
+
+  addMenuItem(menuItem, quantity) {
+    this.#menuItems.set(menuItem.name, { ...menuItem, quantity });
+    this.#totalAmount += menuItem.price * quantity;
+  }
+
+  addDiscountDetail(discountName, discountAmount) {
+    this.#discountDetails[discountName] =
+      (this.#discountDetails[discountName] || 0) + discountAmount;
+    this.#discountAmount += discountAmount;
+  }
+
+  setDiscountDetails(discountDetails) {
+    this.#discountDetails = { ...discountDetails };
+    this.#discountAmount = Object.values(this.#discountDetails).reduce(
+      (sum, amount) => sum + amount,
+      0,
+    );
+  }
+
+  checkForGift() {
+    if (this.#totalAmount >= OrderConstants.GIFT_THRESHOLD_AMOUNT) {
+      this.#gift = `${OrderConstants.GIFT_NAME} ${OrderConstants.GIFT_QUANTITY}개`;
+    }
+  }
+
+  getDiscountDetails() {
+    return this.#discountDetails;
+  }
+
+  getTotalAmount() {
+    return this.#totalAmount;
+  }
+
+  getFinalAmount() {
+    const giftDiscount = this.#discountDetails[OrderConstants.GIFT_EVENT_TITLE] || 0;
+    return this.#totalAmount - (this.#discountAmount - giftDiscount);
+  }
+
+  setDiscountAmount(discountAmount) {
+    this.#discountAmount = discountAmount;
+  }
+
+  getDiscountAmount() {
+    return this.#discountAmount;
+  }
+
+  getItems() {
+    return Array.from(this.#menuItems.values());
+  }
+
+  getGift() {
+    return this.#gift;
+  }
+}

리뷰: 저는 `Model`은 증정 메뉴에 대한 정보(데이터)만 저장하고, 그 데이터를 가지고 '`증정메뉴` `개수`개'와 같이 포맷팅하는 것은 `View`의 역할이라고 생각해요.

만약 증정 메뉴 데이터에 대해 다른 형식으로 출력하는 뷰가 여러 개 생긴다고 가정해 봅시다. 지금 코드방식대로라면 각 뷰에서 나타낼 출력 형식을 `Model`에서 수행해야 합니다. 하지만 이건 `Model`이 `View`에 의존하는 것과 같습니다. 출력 형식이 변경되었을 때 `Model`의 메서드를 변경하게 될 테니까요. 또 `Model`의 역할을 벗어나는 것이기도 합니다.

그래서 저는 이 메서드를 출력을 담당하는 `View` 쪽으로 옮기는 게 좋을 것 같은데, 어떻게 생각하시나요?
@@ -0,0 +1,6 @@
+export const OrderConstants = {
+  GIFT_THRESHOLD_AMOUNT: 120000,
+  GIFT_EVENT_TITLE: '증정 이벤트',
+  GIFT_NAME: '샴페인',
+  GIFT_QUANTITY: 1,
+};

리뷰: 지금은 증정 이벤트가 샴페인 1개만 주고 있는데요. 만약 이 이벤트가 확장된다면, 배지 이벤트처럼 금액에 따라 다른 선물을 증정하게 될 수도 있을 것 같아요. Strategy 패턴을 선택하신 이유도 유지보수와 변경에 용이해서라고 생각되는데요. 그만큼 상수를 선언할 때에도 확장성을 챙기면 더 좋지 않았을까 생각해봅니다!
@@ -0,0 +1,46 @@
+import ChristmasDiscountStrategy from './strategies/ChristmasDiscountStrategy';
+import WeekdayDiscountStrategy from './strategies/WeekdayDiscountStrategy';
+import WeekendDiscountStrategy from './strategies/WeekendDiscountStrategy';
+import SpecialDiscountStrategy from './strategies/SpecialDiscountStrategy';
+import GiftChampagneStrategy from './strategies/GiftChampagneStrategy';
+
+export default class DiscountService {
+  #strategies;
+
+  constructor() {
+    this.#strategies = {
+      christmas: new ChristmasDiscountStrategy(),
+      special: new SpecialDiscountStrategy(),
+      weekday: new WeekdayDiscountStrategy(),
+      weekend: new WeekendDiscountStrategy(),
+      giftChampagne: new GiftChampagneStrategy(),
+    };
+  }
+
+  applyDiscounts(order, date) {
+    const MINIMUM_AMOUNT_FOR_DISCOUNTS = 10000;
+    if (order.getTotalAmount() >= MINIMUM_AMOUNT_FOR_DISCOUNTS) {
+      const discountResults = this.calculateTotalDiscounts(order, date);
+      const totalDiscountAmount = this.calculateTotalDiscountAmount(discountResults);
+      order.setDiscountAmount(totalDiscountAmount);
+      return discountResults;
+    }
+    return {};
+  }
+
+  calculateTotalDiscounts(order, date) {
+    const discountResults = {};
+    Object.keys(this.#strategies).forEach((strategyKey) => {
+      const strategy = this.#strategies[strategyKey];
+      const discountResult = strategy.applyDiscount(order, date);
+      if (discountResult.amount > 0) {
+        discountResults[discountResult.name] = discountResult.amount;
+      }
+    });
+    return discountResults;
+  }
+
+  calculateTotalDiscountAmount(discountResults) {
+    return Object.values(discountResults).reduce((sum, amount) => sum + amount, 0);
+  }
+}

리뷰: 이 부분을 이렇게 바꿀 수 있을 것 같아요!

```javascript
Object.values(this.#strategies).forEach((strategy) => {
  const discountResult = strategy.applyDiscount(order, date);
  // ...
});
```
@@ -0,0 +1,5 @@
+export default class IDiscountStrategy {
+  applyDiscount(order, date) {
+    throw new Error('[ERROR] 하위 클래스에서 applyDiscount 메서드를 구현해야 합니다.');
+  }
+}

리뷰: 파일명과 클래스명 맨 앞에 `I`가 붙어 있는데, 혹시 의도하신 건가요? 의도하신 거라면 어떤 의미인지 궁금합니다!
@@ -0,0 +1,16 @@
+import IDiscountStrategy from './IDiscountStrategy';
+import { DiscountStrategyConstants } from './constants/DiscountStrategyConstants';
+
+export default class SpecialDiscountStrategy extends IDiscountStrategy {
+  applyDiscount(order, date) {
+    const dayOfWeek = date.getDay();
+    const isSunday = dayOfWeek === 0;
+    const isChristmas = date.getDate() === 25;
+
+    if (isSunday || isChristmas) {
+      return { name: DiscountStrategyConstants.SPECIAL_DISCOUNT, amount: 1000 };
+    }
+
+    return { name: DiscountStrategyConstants.SPECIAL_DISCOUNT, amount: 0 };
+  }
+}

리뷰: 특별 할인이 모두 일요일 (+25일)이라는 점을 이용하셨네요!

저는 별 표시가 된 날짜를 하나하나 상수 배열에 담아서 관리했는데요. 옮겨 적을 때 오타라도 난다면 큰일이지만😅, 이렇게 한 이유는 특별 할인 날짜가 변경될 수 있을 것 같다고 생각했기 때문입니다. 요구사항에서 "특별 할인"을 설명할 때, '달력에 별이 표시된 날짜'라고 설명하더라구요. 만약 '모든 일요일과 크리스마스 당일'처럼 설명했다면 저도 이런 코드 방식으로 구현했을 것 같네요.

구현 방식에 정답은 없지만 제 생각을 한 번 공유드리고 싶었어요😄