ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,50 @@
+package christmas.domain;
+
+import static christmas.exception.ErrorMessages.INVALID_ORDER;
+
+import christmas.utils.IntegerConverter;
+import christmas.exception.InvalidOrderException;
+
+public class Order {
+    public static final int MIN_ORDER_QUANTITY = 1;
+
+    private final Menu menu;
+    private final int quantity;
+
+    public Order(String menuName, String quantity) {
+        this.menu = Menu.from(menuName);
+        this.quantity = convertType(quantity);
+
+        validateMinQuantity();
+    }
+
+    private int convertType(String quantity) {
+        return IntegerConverter.convert(quantity, INVALID_ORDER);
+    }
+
+    private void validateMinQuantity() {
+        if (quantity < MIN_ORDER_QUANTITY) {
+            throw new InvalidOrderException();
+        }
+    }
+
+    public boolean isEqualsMenuType(MenuType type) {
+        return menu.isEqualsMenuType(type);
+    }
+
+    public int calculatePrice() {
+        return menu.calculatePrice(quantity);
+    }
+
+    public String getMenuName() {
+        return menu.getName();
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public Menu getMenu() {
+        return menu;
+    }
+}

리뷰: @Libienz 리뷰에 적극 공감하는 바입니다! 입력에 대한 형식은 View 계층에서 검증하는 것이 더 좋다고 생각되네요. 현재는 입력값을 그대로 생성자 인자로 전달하기 때문에 도메인 객체 내부에서 형식에 맞춰 값을 파싱하는 로직을 수행하고 있어요. @Libienz 말씀처럼 이러한 구조는 도메인의 책임을 흐릴 수 있다고 생각해요. 추가로, 파싱 로직에 의해 도메인 로직을 한 눈에 파악할 수 없다는 단점이 있을거 같아요. 

근희님 코드를 보니 입력값에 대한 형식 검증 및 파싱 객체가 분리되어 있더라구요. 정말 인상깊게 봤습니다. 저도 앞으로는 View 계층으로 해당 로직을 분리해 볼 생각입니다. 좋은 리뷰 감사합니다 👍🏻
@@ -0,0 +1,48 @@
+package christmas.domain;
+
+import christmas.exception.InvalidDateException;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.util.List;
+
+public class VisitDate {
+    private static final int YEAR = 2023;
+    public static final int MONTH = 12;
+    private static final int MIN_DATE = 1;
+    private static final int MAX_DATE = 31;
+
+    private final int date;
+
+    public VisitDate(int date) {
+        validateDateRange(date);
+        this.date = date;
+    }
+
+    private void validateDateRange(int date) {
+        if (date < MIN_DATE || date > MAX_DATE) {
+            throw new InvalidDateException();
+        }
+    }
+
+    public DayOfWeek getDayOfWeek() {
+        LocalDate localDate = LocalDate.of(YEAR, MONTH, date);
+        return localDate.getDayOfWeek();
+    }
+
+    public boolean isContainDayOfDay(List<DayOfWeek> weekDay) {
+        DayOfWeek dayOfWeek = getDayOfWeek();
+        return weekDay.contains(dayOfWeek);
+    }
+
+    public boolean isEqualsDate(int date) {
+        return this.date == date;
+    }
+
+    public boolean isNotAfter(int date) {
+        return this.date <= date;
+    }
+
+    public int calculateDiscount(int discountAmount) {
+        return (date - 1) * discountAmount;
+    }
+}

리뷰: 엇 이 메소드는 VisitDate 내부에서만 사용하는 메소드인데 접근 제한자를 잘못 설정했군요 .. 세세한 리뷰 감사합니다 :)
@@ -0,0 +1,53 @@
+package christmas.domain;
+
+import christmas.dto.BenefitDto;
+import christmas.dto.GiftsDto;
+import java.util.function.Supplier;
+
+public class Bill {
+    public static final int MINIMUM_ORDERS_PRICE = 10000;
+    public static final int EVENT_NOT_APPLIED_AMOUNT = 0;
+
+    private final Discounts discounts;
+    private final Gifts gifts;
+
+    public Bill(Discounts discounts, Gifts gifts) {
+        this.discounts = discounts;
+        this.gifts = gifts;
+    }
+
+    public int getTotalBenefit(Orders orders) {
+        return getAmount(this::sumBenefit, orders);
+    }
+
+    public int getDiscountPrice(Orders orders) {
+        return orders.getTotalPrice() - discounts.sumDiscount();
+    }
+
+    public GiftsDto getGiftDto() {
+        return new GiftsDto(gifts.getResult());
+    }
+
+    public BenefitDto getBenefitDto() {
+        return new BenefitDto(discounts.getResult(), gifts.sumPrice());
+    }
+
+    public String getBadgeName() {
+        return Badge.getNameByBenefit(sumBenefit());
+    }
+
+    private int sumBenefit() {
+        return discounts.sumDiscount() + gifts.sumPrice();
+    }
+
+    private boolean isEventApplicable(Orders orders) {
+        return orders.getTotalPrice() >= MINIMUM_ORDERS_PRICE;
+    }
+
+    private <T> int getAmount(Supplier<T> supplier, Orders orders) {
+        if (isEventApplicable(orders)) {
+            return (int) supplier.get();
+        }
+        return EVENT_NOT_APPLIED_AMOUNT;
+    }
+}

리뷰: 와우 도메인과 책임과 역할에 대해서 깊은 고민을 하신 것이 느껴지네요.. 👍🏻 👍🏻 책임과 역할을 분리하고 부여하는 것은 저도 익숙치가 않은데 정성스러운 답변 주셔서 저도 저만의 기준을 한번 검토해보아야겠다는 생각이 드네요 😄
@@ -0,0 +1,55 @@
+package christmas.controller;
+
+import static christmas.utils.RepeatReader.repeatRead;
+
+import christmas.domain.Bill;
+import christmas.domain.Orders;
+import christmas.domain.Restaurant;
+import christmas.domain.VisitDate;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+
+public class EventController {
+    private final Restaurant restaurant;
+    private final InputView inputView;
+    private final OutputView outputView;
+
+    public EventController(Restaurant restaurant, InputView inputView, OutputView outputView) {
+        this.restaurant = restaurant;
+        this.inputView = inputView;
+        this.outputView = outputView;
+    }
+
+    public void run() {
+        VisitDate visitDate = repeatRead(this::readDate);
+        Orders orders = repeatRead(this::readOrders);
+
+        Bill bill = restaurant.order(visitDate, orders);
+
+        printOrders(orders);
+        printBill(bill, orders);
+    }
+
+    private VisitDate readDate() {
+        int date = inputView.readDate();
+        return new VisitDate(date);
+    }
+
+    private Orders readOrders() {
+        String orders = inputView.readOrders();
+        return new Orders(orders);
+    }
+
+    private void printOrders(Orders orders) {
+        outputView.printOrders(orders.getOrders());
+        outputView.printPrice(orders.getTotalPrice());
+    }
+
+    private void printBill(Bill bill, Orders orders) {
+        outputView.printGifts(bill.getGiftDto());
+        outputView.printBenefit(bill.getBenefitDto());
+        outputView.printBenefitAmount(bill.getTotalBenefit(orders));
+        outputView.printDiscountPrice(bill.getDiscountPrice(orders));
+        outputView.printBadge(bill.getBadgeName());
+    }
+}

리뷰: 우와.. 전반적으로 코드 흐름이 잘 읽히네요..!!
할인 결과를 보여주는 함수의 이름을 어떻게 해야할까 고민을 많이 했는데, printBill()이라는 이름을 보니 
머리에 망치를 한 대 맞은 것 같습니다.. 👍
@@ -0,0 +1,84 @@
+import { Console } from '@woowacourse/mission-utils';
+import { PROMOTION_CATEGORIES } from '../constant/index.js';
+
+const MESSAGE = {
+    GREETINGS: '안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.',
+    INTRO_PREVIEW: '12월 26일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!\n',
+    ORDER_MENU_TITLE: '<주문 메뉴>',
+    TOTAL_PRICE_WITHOUT_DISCOUNT_TITLE: '<할인 전 총주문 금액>',
+    BENEFIT_DETAILS_TITLE: '<혜택 내역>',
+    TOTAL_PRICE_WITH_DISCOUNT_TITLE: '<할인 후 예상 결제 금액>',
+    GIFT_TITLE: '<증정 메뉴>',
+    BENEFIT_TOTAL_PRICE_TITLE: '<총혜택 금액>',
+    BADGE_TITLE: '<12월 이벤트 배지>',
+    NONE: '없음',
+    MENU: (name, count) => `${name} ${count}개`,
+    PRICE: (price) => `${price.toLocaleString()}원`,
+    GIFT: (name, count) => `${name} ${count}개`,
+    DISCOUNT: (name, price) => `${name} 할인: -${price.toLocaleString()}원`,
+    TOTAL_BENEFIT_PRICE: (price) => (price > 0 ? `-${price.toLocaleString()}원` : '0원'),
+};
+
+const OutputView = {
+    printGreetings() {
+        Console.print(MESSAGE.GREETINGS);
+    },
+    printPreviewMessage() {
+        Console.print(MESSAGE.INTRO_PREVIEW);
+        Console.print('');
+    },
+    printOrderMenus(orders) {
+        Console.print(MESSAGE.ORDER_MENU_TITLE);
+
+        orders.forEach((order) => {
+            Console.print(MESSAGE.MENU(order.name, order.count));
+        });
+
+        Console.print('');
+    },
+    printTotalPriceWithoutDiscount(totalPrice) {
+        Console.print(MESSAGE.TOTAL_PRICE_WITHOUT_DISCOUNT_TITLE);
+        Console.print(MESSAGE.PRICE(totalPrice));
+        Console.print('');
+    },
+    printBenefitDetails(promotions) {
+        Console.print(MESSAGE.BENEFIT_DETAILS_TITLE);
+
+        const printData = [];
+
+        promotions.forEach((promotion) => {
+            const { NAME, promotionBenefitPrice } = promotion;
+            const isApplied = promotionBenefitPrice > 0;
+            if (isApplied) printData.push(MESSAGE.DISCOUNT(NAME, promotionBenefitPrice));
+        });
+        if (printData.length === 0) printData.push(MESSAGE.NONE);
+
+        Console.print(printData.join('\n'));
+        Console.print('');
+    },
+    printTotalPriceWithDiscount(totalPriceWithDiscount) {
+        Console.print(MESSAGE.TOTAL_PRICE_WITH_DISCOUNT_TITLE);
+        Console.print(MESSAGE.PRICE(totalPriceWithDiscount));
+        Console.print('');
+    },
+    printGiveWayMenu(promotions) {
+        const giftMenu = promotions.find((promotion) => promotion.EVENT === PROMOTION_CATEGORIES.GIFT);
+        const { PRODUCT, promotionBenefitPrice } = giftMenu;
+        const isApplied = promotionBenefitPrice > 0;
+
+        Console.print(MESSAGE.GIFT_TITLE);
+        Console.print(isApplied ? MESSAGE.GIFT(PRODUCT.NAME, 1) : MESSAGE.NONE);
+        Console.print('');
+    },
+    printTotalBenefitPrice(totalBenefitPrice) {
+        Console.print(MESSAGE.BENEFIT_TOTAL_PRICE_TITLE);
+        Console.print(MESSAGE.TOTAL_BENEFIT_PRICE(totalBenefitPrice));
+        Console.print('');
+    },
+    printBadge(badge) {
+        Console.print(MESSAGE.BADGE_TITLE);
+        Console.print(badge);
+    },
+};
+
+export default OutputView;

리뷰: 저도 이 부분 실수했는데, 일자가 바뀌어서 출력되도록 해야해요..ㅜㅜ
@@ -1,5 +1,42 @@
+import { InputView, OutputView } from './view/index.js';
+import { MENUS, PROMOTION_MONTH, PROMOTION_YEAR } from './constant/index.js';
+import { Order, Planner, PromotionCalendar, Promotions } from './model/index.js';
+
 class App {
-  async run() {}
+    async run() {
+        OutputView.printGreetings();
+        const { order, planner } = await this.reserve();
+        OutputView.printPreviewMessage();
+        await this.preview(order, planner);
+    }
+
+    async reserve() {
+        const orderDate = await InputView.readOrderDate((input) => Order.validateDate(input));
+        const orderMenu = await InputView.readOrderMenus((input) => Order.validateOrder(input));
+        const order = new Order(MENUS, orderMenu, orderDate);
+        const planner = new Planner(order, new PromotionCalendar(PROMOTION_YEAR, PROMOTION_MONTH, Promotions));
+
+        return {
+            order,
+            planner,
+        };
+    }
+
+    async preview(order, planner) {
+        const orderMenuList = order.getOrderMenuList();
+        const totalPriceWithoutDiscount = order.getTotalPrice();
+        const promotions = planner.getPromotionsByOrderDate();
+        const totalBenefitPrice = planner.getTotalBenefitPrice();
+        const totalPriceWithDiscount = planner.getTotalPriceWithDiscount();
+        const badge = planner.getBadge();
+        OutputView.printOrderMenus(orderMenuList);
+        OutputView.printTotalPriceWithoutDiscount(totalPriceWithoutDiscount);
+        OutputView.printGiveWayMenu(promotions);
+        OutputView.printBenefitDetails(promotions);
+        OutputView.printTotalBenefitPrice(totalBenefitPrice);
+        OutputView.printTotalPriceWithDiscount(totalPriceWithDiscount);
+        OutputView.printBadge(badge);
+    }
 }
 
 export default App;

리뷰: 지극히 개인적인 의견인데, 메서드명을 '예약'이라고 지으셨는데 oderDate는 조금 혼란을 줄 수 있는 것 같아요.
'방문'이나 '예약'으로 변경하시는 게 어떨까요..?
@@ -0,0 +1,84 @@
+import { Console } from '@woowacourse/mission-utils';
+import { PROMOTION_CATEGORIES } from '../constant/index.js';
+
+const MESSAGE = {
+    GREETINGS: '안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.',
+    INTRO_PREVIEW: '12월 26일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!\n',
+    ORDER_MENU_TITLE: '<주문 메뉴>',
+    TOTAL_PRICE_WITHOUT_DISCOUNT_TITLE: '<할인 전 총주문 금액>',
+    BENEFIT_DETAILS_TITLE: '<혜택 내역>',
+    TOTAL_PRICE_WITH_DISCOUNT_TITLE: '<할인 후 예상 결제 금액>',
+    GIFT_TITLE: '<증정 메뉴>',
+    BENEFIT_TOTAL_PRICE_TITLE: '<총혜택 금액>',
+    BADGE_TITLE: '<12월 이벤트 배지>',
+    NONE: '없음',
+    MENU: (name, count) => `${name} ${count}개`,
+    PRICE: (price) => `${price.toLocaleString()}원`,
+    GIFT: (name, count) => `${name} ${count}개`,
+    DISCOUNT: (name, price) => `${name} 할인: -${price.toLocaleString()}원`,
+    TOTAL_BENEFIT_PRICE: (price) => (price > 0 ? `-${price.toLocaleString()}원` : '0원'),
+};
+
+const OutputView = {
+    printGreetings() {
+        Console.print(MESSAGE.GREETINGS);
+    },
+    printPreviewMessage() {
+        Console.print(MESSAGE.INTRO_PREVIEW);
+        Console.print('');
+    },
+    printOrderMenus(orders) {
+        Console.print(MESSAGE.ORDER_MENU_TITLE);
+
+        orders.forEach((order) => {
+            Console.print(MESSAGE.MENU(order.name, order.count));
+        });
+
+        Console.print('');
+    },
+    printTotalPriceWithoutDiscount(totalPrice) {
+        Console.print(MESSAGE.TOTAL_PRICE_WITHOUT_DISCOUNT_TITLE);
+        Console.print(MESSAGE.PRICE(totalPrice));
+        Console.print('');
+    },
+    printBenefitDetails(promotions) {
+        Console.print(MESSAGE.BENEFIT_DETAILS_TITLE);
+
+        const printData = [];
+
+        promotions.forEach((promotion) => {
+            const { NAME, promotionBenefitPrice } = promotion;
+            const isApplied = promotionBenefitPrice > 0;
+            if (isApplied) printData.push(MESSAGE.DISCOUNT(NAME, promotionBenefitPrice));
+        });
+        if (printData.length === 0) printData.push(MESSAGE.NONE);
+
+        Console.print(printData.join('\n'));
+        Console.print('');
+    },
+    printTotalPriceWithDiscount(totalPriceWithDiscount) {
+        Console.print(MESSAGE.TOTAL_PRICE_WITH_DISCOUNT_TITLE);
+        Console.print(MESSAGE.PRICE(totalPriceWithDiscount));
+        Console.print('');
+    },
+    printGiveWayMenu(promotions) {
+        const giftMenu = promotions.find((promotion) => promotion.EVENT === PROMOTION_CATEGORIES.GIFT);
+        const { PRODUCT, promotionBenefitPrice } = giftMenu;
+        const isApplied = promotionBenefitPrice > 0;
+
+        Console.print(MESSAGE.GIFT_TITLE);
+        Console.print(isApplied ? MESSAGE.GIFT(PRODUCT.NAME, 1) : MESSAGE.NONE);
+        Console.print('');
+    },
+    printTotalBenefitPrice(totalBenefitPrice) {
+        Console.print(MESSAGE.BENEFIT_TOTAL_PRICE_TITLE);
+        Console.print(MESSAGE.TOTAL_BENEFIT_PRICE(totalBenefitPrice));
+        Console.print('');
+    },
+    printBadge(badge) {
+        Console.print(MESSAGE.BADGE_TITLE);
+        Console.print(badge);
+    },
+};
+
+export default OutputView;

리뷰: 으아 그러네요 ㅠㅠㅠ 생각지도 못했네요 ㅠㅠ
@@ -1,5 +1,42 @@
+import { InputView, OutputView } from './view/index.js';
+import { MENUS, PROMOTION_MONTH, PROMOTION_YEAR } from './constant/index.js';
+import { Order, Planner, PromotionCalendar, Promotions } from './model/index.js';
+
 class App {
-  async run() {}
+    async run() {
+        OutputView.printGreetings();
+        const { order, planner } = await this.reserve();
+        OutputView.printPreviewMessage();
+        await this.preview(order, planner);
+    }
+
+    async reserve() {
+        const orderDate = await InputView.readOrderDate((input) => Order.validateDate(input));
+        const orderMenu = await InputView.readOrderMenus((input) => Order.validateOrder(input));
+        const order = new Order(MENUS, orderMenu, orderDate);
+        const planner = new Planner(order, new PromotionCalendar(PROMOTION_YEAR, PROMOTION_MONTH, Promotions));
+
+        return {
+            order,
+            planner,
+        };
+    }
+
+    async preview(order, planner) {
+        const orderMenuList = order.getOrderMenuList();
+        const totalPriceWithoutDiscount = order.getTotalPrice();
+        const promotions = planner.getPromotionsByOrderDate();
+        const totalBenefitPrice = planner.getTotalBenefitPrice();
+        const totalPriceWithDiscount = planner.getTotalPriceWithDiscount();
+        const badge = planner.getBadge();
+        OutputView.printOrderMenus(orderMenuList);
+        OutputView.printTotalPriceWithoutDiscount(totalPriceWithoutDiscount);
+        OutputView.printGiveWayMenu(promotions);
+        OutputView.printBenefitDetails(promotions);
+        OutputView.printTotalBenefitPrice(totalBenefitPrice);
+        OutputView.printTotalPriceWithDiscount(totalPriceWithDiscount);
+        OutputView.printBadge(badge);
+    }
 }
 
 export default App;

리뷰: 아 맞네요 그게 더 좋을 것 같습니다 감사합니다
@@ -1,5 +1,42 @@
+import { InputView, OutputView } from './view/index.js';
+import { MENUS, PROMOTION_MONTH, PROMOTION_YEAR } from './constant/index.js';
+import { Order, Planner, PromotionCalendar, Promotions } from './model/index.js';
+
 class App {
-  async run() {}
+    async run() {
+        OutputView.printGreetings();
+        const { order, planner } = await this.reserve();
+        OutputView.printPreviewMessage();
+        await this.preview(order, planner);
+    }
+
+    async reserve() {
+        const orderDate = await InputView.readOrderDate((input) => Order.validateDate(input));
+        const orderMenu = await InputView.readOrderMenus((input) => Order.validateOrder(input));
+        const order = new Order(MENUS, orderMenu, orderDate);
+        const planner = new Planner(order, new PromotionCalendar(PROMOTION_YEAR, PROMOTION_MONTH, Promotions));
+
+        return {
+            order,
+            planner,
+        };
+    }
+
+    async preview(order, planner) {
+        const orderMenuList = order.getOrderMenuList();
+        const totalPriceWithoutDiscount = order.getTotalPrice();
+        const promotions = planner.getPromotionsByOrderDate();
+        const totalBenefitPrice = planner.getTotalBenefitPrice();
+        const totalPriceWithDiscount = planner.getTotalPriceWithDiscount();
+        const badge = planner.getBadge();
+        OutputView.printOrderMenus(orderMenuList);
+        OutputView.printTotalPriceWithoutDiscount(totalPriceWithoutDiscount);
+        OutputView.printGiveWayMenu(promotions);
+        OutputView.printBenefitDetails(promotions);
+        OutputView.printTotalBenefitPrice(totalBenefitPrice);
+        OutputView.printTotalPriceWithDiscount(totalPriceWithDiscount);
+        OutputView.printBadge(badge);
+    }
 }
 
 export default App;

리뷰: 진입점 코드에서 전체적인 흐름 파악이 어렵다는 생각이 듭니다.
`printPreviewMessage()`, `this.preview` 함수들이 무슨 일을 하는지 잘 예상이 안 가요.
@@ -0,0 +1,55 @@
+import { BADGE, PROMOTION_CATEGORIES, PROMOTION_MINIMUM_PRICE } from '../constant/index.js';
+
+class Planner {
+    #order;
+    #calendar;
+
+    constructor(order, calendar) {
+        this.#order = order;
+        this.#calendar = calendar;
+    }
+
+    getPromotionsByOrderDate() {
+        const promotions = this.#calendar.getPromotionsByDate(this.#order.getOrderDate());
+        const activePromotions = [];
+        promotions.forEach((promotion) => {
+            const { CONFIG } = promotion;
+            activePromotions.push({
+                ...CONFIG,
+                promotionBenefitPrice: this.#applyPromotions(promotion),
+            });
+        });
+
+        return activePromotions;
+    }
+    getTotalBenefitPrice() {
+        const promotions = this.getPromotionsByOrderDate();
+
+        return promotions.reduce((acc, cur) => acc + cur.promotionBenefitPrice, 0);
+    }
+    getTotalPriceWithDiscount() {
+        const promotions = this.getPromotionsByOrderDate();
+        const totalPrice = this.#order.getTotalPrice();
+        const getTotalBenefitPrice = this.getTotalBenefitPrice();
+        const giftPromotion = promotions.find((promotion) => promotion.EVENT === PROMOTION_CATEGORIES.GIFT);
+
+        return totalPrice - (getTotalBenefitPrice - giftPromotion.promotionBenefitPrice);
+    }
+    getBadge() {
+        const { SANTA, TREE, STAR, NONE } = BADGE;
+        const totalBenefitPrice = this.getTotalBenefitPrice();
+        if (totalBenefitPrice >= SANTA.PRICE) return SANTA.NAME;
+        if (totalBenefitPrice >= TREE.PRICE) return TREE.NAME;
+        if (totalBenefitPrice >= STAR.PRICE) return STAR.NAME;
+
+        return NONE.NAME;
+    }
+    #applyPromotions(promotion) {
+        const totalPriceWithoutDiscount = this.#order.getTotalPrice();
+        if (totalPriceWithoutDiscount < PROMOTION_MINIMUM_PRICE) return 0;
+
+        return promotion.getPromotionPrice(this.#order);
+    }
+}
+
+export default Planner;

리뷰: Planner가 너무 많은 객체에 의존하는 것 같아요.
또, 어떤 객체인지 잘 파악이 안 돼요.
Planner 라는 이름과, 메서드가 매칭이 잘 안되네요
@@ -0,0 +1,45 @@
+import { PROMOTIONS } from '../constant/index.js';
+
+const ChristPromotion = {
+    CONFIG: PROMOTIONS.CHRISTMAS,
+    getPromotionPrice(order) {
+        const date = order.getOrderDate();
+
+        return this.CONFIG.BENEFIT_PRICE + this.CONFIG.BENEFIT_PRICE * 0.1 * (date - 1);
+    },
+};
+const WeekendsPromotion = {
+    CONFIG: PROMOTIONS.WEEKENDS,
+    getPromotionPrice(order) {
+        const productInOrder = order.getOrderMenuByCategory(this.CONFIG.PRODUCT);
+        const menuCount = productInOrder.reduce((acc, cur) => acc + Number(cur.count), 0);
+
+        return this.CONFIG.BENEFIT_PRICE * menuCount;
+    },
+};
+const WeekdaysPromotion = {
+    CONFIG: PROMOTIONS.WEEKDAYS,
+    getPromotionPrice(order) {
+        const productInOrder = order.getOrderMenuByCategory(this.CONFIG.PRODUCT);
+        const menuCount = productInOrder.reduce((acc, cur) => acc + Number(cur.count), 0);
+
+        return this.CONFIG.BENEFIT_PRICE * menuCount;
+    },
+};
+const SpecialPromotion = {
+    CONFIG: PROMOTIONS.SPECIAL,
+    getPromotionPrice() {
+        return this.CONFIG.BENEFIT_PRICE;
+    },
+};
+const GiftPromotion = {
+    CONFIG: PROMOTIONS.GIFT,
+    getPromotionPrice(order) {
+        const preDiscountAmount = order.getTotalPrice();
+        if (preDiscountAmount < this.CONFIG.MINIMUM_PRICE) return 0;
+
+        return this.CONFIG.BENEFIT_PRICE;
+    },
+};
+export const Promotions = [ChristPromotion, WeekendsPromotion, WeekdaysPromotion, SpecialPromotion, GiftPromotion];
+export default Promotions;

리뷰: `CONFIG`가 뭘 뜻하는지 추측이 잘 안되네요.
마찬가지로 `PROMOTIONS.CHRISTMAS`에 뭐가 들어있는지 예상이 안가요.

상수들을 조금더 단순화하는게 필요하다고 생각합니다.
@@ -0,0 +1,13 @@
+const PREFIX_ERROR = '[ERROR]';
+
+export const check = (errorMessage, validator) => {
+    if (validator()) throw new Error(`${PREFIX_ERROR} ${errorMessage}`);
+};
+export const isMoreThanLimit = (errorMessage, target, limit) => check(errorMessage, () => target > limit);
+export const isDuplicate = (errorMessage, targets) =>
+    check(errorMessage, () => new Set(targets).size !== targets.length);
+export const isNotMatchRegex = (errorMessage, target, regex) => check(errorMessage, () => !regex.test(target));
+export const isEveryInclude = (errorMessage, targets, references) =>
+    check(errorMessage, () => targets.every((item) => references.includes(item)));
+export const isSomeNotInclude = (errorMessage, targets, references) =>
+    check(errorMessage, () => targets.some((item) => !references.includes(item)));

리뷰: constant 폴더로 따로 분리하하지 않고 여기서 선언하신 이유가 있나요?