ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,31 @@
+import {
+  ModalBody,
+  ModalButtonContainer,
+  ModalCloseButton,
+  ModalContainer,
+  ModalDescription,
+  ModalDimmedLayer,
+  ModalHeader,
+  ModalInputField,
+  ModalTitle,
+} from './index';
+
+interface ModalProp {
+  children: JSX.Element;
+}
+
+const Modal = ({ children }: ModalProp) => {
+  return <>{children}</>;
+};
+
+Modal.Body = ModalBody;
+Modal.ButtonContainer = ModalButtonContainer;
+Modal.CloseButton = ModalCloseButton;
+Modal.Container = ModalContainer;
+Modal.Description = ModalDescription;
+Modal.DimmedLayer = ModalDimmedLayer;
+Modal.Header = ModalHeader;
+Modal.InputField = ModalInputField;
+Modal.Title = ModalTitle;
+
+export default Modal;

리뷰: 이부분을 위해서 compound component를 적용하려고 했고, 장점을 잘 못살렸다고 생각했는데
수야가 제 의도를 완벽히 파악하신걸 보니, 그렇지만은 않았던 것 같네요! 😃
@@ -0,0 +1,31 @@
+import {
+  ModalBody,
+  ModalButtonContainer,
+  ModalCloseButton,
+  ModalContainer,
+  ModalDescription,
+  ModalDimmedLayer,
+  ModalHeader,
+  ModalInputField,
+  ModalTitle,
+} from './index';
+
+interface ModalProp {
+  children: JSX.Element;
+}
+
+const Modal = ({ children }: ModalProp) => {
+  return <>{children}</>;
+};
+
+Modal.Body = ModalBody;
+Modal.ButtonContainer = ModalButtonContainer;
+Modal.CloseButton = ModalCloseButton;
+Modal.Container = ModalContainer;
+Modal.Description = ModalDescription;
+Modal.DimmedLayer = ModalDimmedLayer;
+Modal.Header = ModalHeader;
+Modal.InputField = ModalInputField;
+Modal.Title = ModalTitle;
+
+export default Modal;

리뷰: 상세한 공유 감사합니다!! 사용해보진 않았지만, 덕분에 어떤 유용함이 있는 지는 확실히 알겠네요!!
@@ -0,0 +1,31 @@
+import {
+  ModalBody,
+  ModalButtonContainer,
+  ModalCloseButton,
+  ModalContainer,
+  ModalDescription,
+  ModalDimmedLayer,
+  ModalHeader,
+  ModalInputField,
+  ModalTitle,
+} from './index';
+
+interface ModalProp {
+  children: JSX.Element;
+}
+
+const Modal = ({ children }: ModalProp) => {
+  return <>{children}</>;
+};
+
+Modal.Body = ModalBody;
+Modal.ButtonContainer = ModalButtonContainer;
+Modal.CloseButton = ModalCloseButton;
+Modal.Container = ModalContainer;
+Modal.Description = ModalDescription;
+Modal.DimmedLayer = ModalDimmedLayer;
+Modal.Header = ModalHeader;
+Modal.InputField = ModalInputField;
+Modal.Title = ModalTitle;
+
+export default Modal;

리뷰: compound component 적용만 안하셨지, 거진 적용된 거나 다름 없을 정도로 완벽한 분리인 것 같습니다 ㅋㅋㅋ 적용하려고 하면 수정 별로 없이 금방 적용될 것 같네요.
@@ -0,0 +1,25 @@
+package oncall.exception;
+
+public enum ExceptionMessage {
+    INVALID_MONTH_DAY_INPUT("월과 요일을 올바르게 입력해주세요."),
+    INVALID_MONTH_INPUT("월은 숫자만 입력 가능합니다."),
+    INVALID_DAY_INPUT("올바르지 않은 요일입니다."),
+    INVALID_DAY_OF_MONTH("해당 월에 시작 요일이 일치하지 않습니다."),
+    INVALID_WORKER_COUNTS("비상 근무 사원을 최소 5명 이상 최대 35명 사이로 입력해주세요."),
+    INVALID_NICKNAME_LENGTH("사원의 닉네임은 최대 5글자까지 가능합니다."),
+    INVALID_NICKNAME_CHARACTER("사원의 닉네임은 한글만 가능합니다."),
+    DUPLICATED_NICKNAME("한 근무에 사원이 중복될 수 없습니다."),
+    WORKER_NOT_MATCHED("평일 사원과 휴일 사원이 일치하지 않습니다."),
+    ;
+
+    private static final String PREFIX = "[ERROR] ";
+    private final String message;
+
+    ExceptionMessage(String message) {
+        this.message = PREFIX + message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: `mesasge`라는 변수명 자체는 안내 문구가 더 적합할 것 같습니다. `getMessage()`를 부를 때 `PREFIX`를 더해 주면 어떨까요?
@@ -0,0 +1,29 @@
+package oncall.domain;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+
+public enum Holiday {
+    NEW_YEAR(LocalDate.of(2023, 1, 1), "신정"),
+    MARCH_FIRST(LocalDate.of(2023, 3, 1), "삼일절"),
+    CHILDREN_DAY(LocalDate.of(2023, 5, 5), "어린이날"),
+    MEMORIAL_DAY(LocalDate.of(2023, 6, 6), "현충일"),
+    LIBERATION_DAY(LocalDate.of(2023, 8, 15), "광복절"),
+    TEN_THIRD(LocalDate.of(2023, 10, 3), "개천절"),
+    KOREAN_DAY(LocalDate.of(2023, 10, 9), "한글날"),
+    CHRISTMAS_DAY(LocalDate.of(2023, 12, 25), "성탄절"),
+    ;
+
+    private final LocalDate date;
+    private final String name;
+
+    Holiday(LocalDate date, String name) {
+        this.date = date;
+        this.name = name;
+    }
+
+    public static boolean isHoliday(LocalDate localDate) {
+        return Arrays.stream(values())
+                .anyMatch(holiday -> holiday.date.equals(localDate));
+    }
+}

리뷰: 공휴일을 Enum으로 관리하니 훨씬 깔끔하네요!
@@ -0,0 +1,16 @@
+package oncall.domain;
+
+import java.util.Collections;
+import java.util.List;
+
+public class EmergencyWorkers {
+    private final List<EmergencyWorker> emergencyWorkers;
+
+    public EmergencyWorkers(List<EmergencyWorker> emergencyWorkers) {
+        this.emergencyWorkers = emergencyWorkers;
+    }
+
+    public List<EmergencyWorker> getEmergencyWorkers() {
+        return Collections.unmodifiableList(emergencyWorkers);
+    }
+}

리뷰: 변경할 수 없더라도, 그 자체를 내주는 것은 지양하면 좋을 것 같습니다. 해당 `List`를 가지고 가서 어떤 연산을 하는지를 파악한 뒤에, 그 연산을 지원하도록 하면 좋을 것 같아요
@@ -1,7 +1,11 @@
 package oncall;
 
+import oncall.view.InputView;
+import oncall.view.OutputView;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        EmergencyWorkPlanner emergencyWorkPlanner = new EmergencyWorkPlanner(new InputView(), new OutputView());
+        emergencyWorkPlanner.run();
     }
 }

리뷰: 가독성을 위해서 각각의 `View` 선언을 한 줄에 하나씩 해줘도 좋을 것 같아요
@@ -0,0 +1,47 @@
+package oncall.view;
+
+import static oncall.view.OutputView.OutputMessage.DATE_WORKER_FORMAT;
+import static oncall.view.OutputView.OutputMessage.HOLIDAY;
+
+import java.time.LocalDate;
+import oncall.domain.Day;
+import oncall.domain.EmergencyWorkers;
+import oncall.domain.Holiday;
+
+public class OutputView {
+
+    public void printEmergencyWork(EmergencyWorkers emergencyWorkers) {
+        emergencyWorkers.getEmergencyWorkers().forEach(worker -> {
+            LocalDate localDate = worker.getLocalDate();
+            String name = worker.getName();
+            String dayOfWeek = Day.of(localDate.getDayOfWeek());
+            if (Holiday.isHoliday(localDate)) {
+                dayOfWeek += HOLIDAY;
+            }
+            printForm(localDate, dayOfWeek, name);
+        });
+    }
+
+    private static void printForm(LocalDate localDate, String dayOfWeek, String name) {
+        System.out.println(
+                String.format(DATE_WORKER_FORMAT.getMessage(), localDate.getMonth().getValue(),
+                        localDate.getDayOfMonth(),
+                        dayOfWeek, name));
+    }
+
+    protected enum OutputMessage {
+        DATE_WORKER_FORMAT("%d월 %d일 %s %s"),
+        HOLIDAY("(휴일)"),
+        ;
+
+        private final String message;
+
+        OutputMessage(String message) {
+            this.message = message;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+    }
+}

리뷰: 한 번에 하는 일이 너무 많은 것 같아요. 메서드를 분리해서 참조를 사용하면 좀 더 간략할 것 같습니다!
@@ -0,0 +1,56 @@
+package oncall.domain;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Worker {
+    private static final int INCREASE_DATE = 1;
+    private static final int OFFSET = 1;
+    private final List<String> weekdayWorkers;
+    private final List<String> holidayWorkers;
+
+    public Worker(List<String> weekdayWorkers, List<String> holidayWorkers) {
+        this.weekdayWorkers = weekdayWorkers;
+        this.holidayWorkers = holidayWorkers;
+    }
+
+    public EmergencyWorkers makeEmergencyWork(LocalDate date) {
+        LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
+        List<EmergencyWorker> emergencyWorkers = new ArrayList<>();
+        int weekdayIndex = 0;
+        int holidayIndex = 0;
+        while (!date.isAfter(lastDayOfMonth)) {
+            if (isWeekend(date) || Holiday.isHoliday(date)) {
+                holidayIndex = assignWorkerAndAdjustIndex(emergencyWorkers, holidayWorkers, holidayIndex, date);
+                date = date.plusDays(INCREASE_DATE);
+                continue;
+            }
+            weekdayIndex = assignWorkerAndAdjustIndex(emergencyWorkers, weekdayWorkers, weekdayIndex, date);
+            date = date.plusDays(INCREASE_DATE);
+        }
+        return new EmergencyWorkers(emergencyWorkers);
+    }
+
+    private boolean isWeekend(LocalDate date) {
+        DayOfWeek dayOfWeek = date.getDayOfWeek();
+        return dayOfWeek.equals(DayOfWeek.SATURDAY) || dayOfWeek.equals(DayOfWeek.SUNDAY);
+    }
+
+    private int assignWorkerAndAdjustIndex(List<EmergencyWorker> emergencyWorkers, List<String> workers, int index,
+                                           LocalDate date) {
+        String worker = workers.get(index);
+        int nextIndex = (index + OFFSET) % workers.size();
+        if (!emergencyWorkers.isEmpty() && emergencyWorkers.get(emergencyWorkers.size() - OFFSET).isSame(worker)) {
+            String nextWorker = workers.get(nextIndex);
+            workers.set(index, nextWorker);
+            workers.set(nextIndex, worker);
+            worker = nextWorker;
+        }
+        emergencyWorkers.add(new EmergencyWorker(worker, date));
+        return nextIndex;
+    }
+}
+

리뷰: 두 조건을 묶어서 하나의 메서드로 나타내도 좋았겠습니다
@@ -0,0 +1,56 @@
+package oncall.domain;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Worker {
+    private static final int INCREASE_DATE = 1;
+    private static final int OFFSET = 1;
+    private final List<String> weekdayWorkers;
+    private final List<String> holidayWorkers;
+
+    public Worker(List<String> weekdayWorkers, List<String> holidayWorkers) {
+        this.weekdayWorkers = weekdayWorkers;
+        this.holidayWorkers = holidayWorkers;
+    }
+
+    public EmergencyWorkers makeEmergencyWork(LocalDate date) {
+        LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
+        List<EmergencyWorker> emergencyWorkers = new ArrayList<>();
+        int weekdayIndex = 0;
+        int holidayIndex = 0;
+        while (!date.isAfter(lastDayOfMonth)) {
+            if (isWeekend(date) || Holiday.isHoliday(date)) {
+                holidayIndex = assignWorkerAndAdjustIndex(emergencyWorkers, holidayWorkers, holidayIndex, date);
+                date = date.plusDays(INCREASE_DATE);
+                continue;
+            }
+            weekdayIndex = assignWorkerAndAdjustIndex(emergencyWorkers, weekdayWorkers, weekdayIndex, date);
+            date = date.plusDays(INCREASE_DATE);
+        }
+        return new EmergencyWorkers(emergencyWorkers);
+    }
+
+    private boolean isWeekend(LocalDate date) {
+        DayOfWeek dayOfWeek = date.getDayOfWeek();
+        return dayOfWeek.equals(DayOfWeek.SATURDAY) || dayOfWeek.equals(DayOfWeek.SUNDAY);
+    }
+
+    private int assignWorkerAndAdjustIndex(List<EmergencyWorker> emergencyWorkers, List<String> workers, int index,
+                                           LocalDate date) {
+        String worker = workers.get(index);
+        int nextIndex = (index + OFFSET) % workers.size();
+        if (!emergencyWorkers.isEmpty() && emergencyWorkers.get(emergencyWorkers.size() - OFFSET).isSame(worker)) {
+            String nextWorker = workers.get(nextIndex);
+            workers.set(index, nextWorker);
+            workers.set(nextIndex, worker);
+            worker = nextWorker;
+        }
+        emergencyWorkers.add(new EmergencyWorker(worker, date));
+        return nextIndex;
+    }
+}
+

리뷰: `EmergencyWorker`는 이곳에서 사용하지 않나요?
@@ -0,0 +1,16 @@
+package oncall.domain;
+
+import java.util.Collections;
+import java.util.List;
+
+public class EmergencyWorkers {
+    private final List<EmergencyWorker> emergencyWorkers;
+
+    public EmergencyWorkers(List<EmergencyWorker> emergencyWorkers) {
+        this.emergencyWorkers = emergencyWorkers;
+    }
+
+    public List<EmergencyWorker> getEmergencyWorkers() {
+        return Collections.unmodifiableList(emergencyWorkers);
+    }
+}

리뷰: 네 맞습니다. DTO를 사용하지 못한 것이 아쉽습니다..
@@ -0,0 +1,56 @@
+package oncall.domain;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Worker {
+    private static final int INCREASE_DATE = 1;
+    private static final int OFFSET = 1;
+    private final List<String> weekdayWorkers;
+    private final List<String> holidayWorkers;
+
+    public Worker(List<String> weekdayWorkers, List<String> holidayWorkers) {
+        this.weekdayWorkers = weekdayWorkers;
+        this.holidayWorkers = holidayWorkers;
+    }
+
+    public EmergencyWorkers makeEmergencyWork(LocalDate date) {
+        LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
+        List<EmergencyWorker> emergencyWorkers = new ArrayList<>();
+        int weekdayIndex = 0;
+        int holidayIndex = 0;
+        while (!date.isAfter(lastDayOfMonth)) {
+            if (isWeekend(date) || Holiday.isHoliday(date)) {
+                holidayIndex = assignWorkerAndAdjustIndex(emergencyWorkers, holidayWorkers, holidayIndex, date);
+                date = date.plusDays(INCREASE_DATE);
+                continue;
+            }
+            weekdayIndex = assignWorkerAndAdjustIndex(emergencyWorkers, weekdayWorkers, weekdayIndex, date);
+            date = date.plusDays(INCREASE_DATE);
+        }
+        return new EmergencyWorkers(emergencyWorkers);
+    }
+
+    private boolean isWeekend(LocalDate date) {
+        DayOfWeek dayOfWeek = date.getDayOfWeek();
+        return dayOfWeek.equals(DayOfWeek.SATURDAY) || dayOfWeek.equals(DayOfWeek.SUNDAY);
+    }
+
+    private int assignWorkerAndAdjustIndex(List<EmergencyWorker> emergencyWorkers, List<String> workers, int index,
+                                           LocalDate date) {
+        String worker = workers.get(index);
+        int nextIndex = (index + OFFSET) % workers.size();
+        if (!emergencyWorkers.isEmpty() && emergencyWorkers.get(emergencyWorkers.size() - OFFSET).isSame(worker)) {
+            String nextWorker = workers.get(nextIndex);
+            workers.set(index, nextWorker);
+            workers.set(nextIndex, worker);
+            worker = nextWorker;
+        }
+        emergencyWorkers.add(new EmergencyWorker(worker, date));
+        return nextIndex;
+    }
+}
+

리뷰: 시험 당시에는 EmergencyWorker 객체로 변환하기 전에 저장해두고 변환하도록 하였습니다. 피드백 감사드립니다 동훈님!
@@ -0,0 +1,84 @@
+import { PROMOTION_DATE_INFO } from '../promotionSystem.js';
+import { SYMBOLS } from '../symbols.js';
+
+export const INPUT_MESSAGE = Object.freeze({
+  visitDate: `${PROMOTION_DATE_INFO.month}월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)\n`,
+  orderMenus:
+    '주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)\n',
+});
+
+export const OUTPUT_MESSAGE = Object.freeze({
+  guideComments: `안녕하세요! 우테코 식당 ${PROMOTION_DATE_INFO.month}월 이벤트 플래너입니다.`,
+
+  title: Object.freeze({
+    orderMenu: '주문 메뉴',
+    totalOrderAmount: '할인 전 총주문 금액',
+    giftMenu: '증정 메뉴',
+    benefitHistory: '혜택 내역',
+    totalBenefitAmount: '총혜택 금액',
+    expectPaymentAmount: '할인 후 예상 결제 금액',
+    eventBadge: `${PROMOTION_DATE_INFO.month}월 이벤트 배지`,
+  }),
+
+  benefitLabels: Object.freeze({
+    xmasBenefitAmount: '크리스마스 디데이 할인',
+    weekDayBenefitAmount: '평일 할인',
+    weekendBenefitAmount: '주말 할인',
+    specialBenefitAmount: '특별 할인',
+    giftAmount: '증정 이벤트',
+  }),
+
+  nothing: '없음',
+
+  gift: '샴페인 1개',
+});
+
+export const FORMAT_MESSAGE = Object.freeze({
+  date(visitDate) {
+    return `${PROMOTION_DATE_INFO.month}월 ${visitDate}일`;
+  },
+
+  endGuideComments(visitDate) {
+    return `${this.date(visitDate)}에 우테코 식당에서 받을 이벤트 혜택 미리 보기!`;
+  },
+
+  title(config, title) {
+    return `${config.newLine ? '\n' : SYMBOLS.emptyString}<${title}>`;
+  },
+
+  orderMenus(orderMenuInfo) {
+    return orderMenuInfo
+      .map(([menuName, quantity]) => `${menuName} ${quantity}개\n`)
+      .join(SYMBOLS.emptyString);
+  },
+
+  amount(amount) {
+    return `${amount.toLocaleString()}원`;
+  },
+
+  benefitHistory(benefitInfo) {
+    return (
+      Object.entries(benefitInfo)
+        .reduce(
+          (acc, [benefitName, amount]) =>
+            amount !== 0
+              ? [...acc, `${OUTPUT_MESSAGE.benefitLabels[benefitName]}: -${this.amount(amount)}`]
+              : acc,
+          [],
+        )
+        .join('\n') || OUTPUT_MESSAGE.nothing
+    );
+  },
+
+  totalBenefitAmount(totalBenefitAmount) {
+    return totalBenefitAmount === 0 ? `${this.amount(0)}` : `-${this.amount(totalBenefitAmount)}`;
+  },
+
+  gift(giftAmount) {
+    return giftAmount === 0 ? OUTPUT_MESSAGE.nothing : OUTPUT_MESSAGE.gift;
+  },
+
+  eventBadge(eventBadge) {
+    return eventBadge ?? OUTPUT_MESSAGE.nothing;
+  },
+});

리뷰: freeze된 객체 안에 함수를 넣어서 return값으로 받을 수 있다는 것도 알게 되었습니다.
응용했다면 많이 간단한 코드를 작성해 볼 수 있었을 것 같습니다
좋은 정보 감사합니다!
@@ -0,0 +1,49 @@
+import InputView from '../views/InputView.js';
+import OutputView from '../views/OutputView.js';
+import PromotionResultService from '../service/PromotionResultService.js';
+import ErrorHandler from '../errors/ErrorHandler/module.js';
+
+/**
+ * @module ChristmasPromotionController
+ * 크리스마스 프로모션 이벤트 관련 애플리케이션의 흐름 제어를 담당
+ */
+const ChristmasPromotionController = {
+  /**
+   * @returns {Promise<void>}
+   */
+  async play() {
+    const { visitDate, orderMenuInfo } = await processUserInput();
+
+    processPromotionResult({ visitDate, orderMenuInfo });
+  },
+};
+
+/**
+ * @returns {Promise<{visitDate : number, orderMenuInfo : [string, number][]}>} 유저가 입력한 방문 일자 및 메뉴 정보를 반환할 Promise 객체
+ */
+async function processUserInput() {
+  OutputView.printStartGuideComments();
+
+  const visitDate = await ErrorHandler.retryOnErrors(InputView.readVisitDate.bind(InputView));
+  const orderMenuInfo = await ErrorHandler.retryOnErrors(
+    InputView.readOrderMenuInfo.bind(InputView),
+  );
+
+  return { visitDate, orderMenuInfo };
+}
+
+/**
+ * @param {{visitDate : number, orderMenuInfo : [string, number][]}} params - 유저가 입력한 방문 일자 및 메뉴 정보
+ * @returns {void}
+ */
+function processPromotionResult({ visitDate, orderMenuInfo }) {
+  const promotionResult = PromotionResultService.createPromotionResult({
+    visitDate,
+    orderMenuInfo,
+  });
+
+  OutputView.printEndGuideComments(visitDate);
+  OutputView.printPromotionResult({ orderMenuInfo, promotionResult });
+}
+
+export default ChristmasPromotionController;

리뷰: [Nit] 취향에서 갈리는 영역이라고 생각하는데 콜백 대신에 동적 this binding을 사용하신 이유가 있으신가요?
@@ -0,0 +1,49 @@
+import InputView from '../views/InputView.js';
+import OutputView from '../views/OutputView.js';
+import PromotionResultService from '../service/PromotionResultService.js';
+import ErrorHandler from '../errors/ErrorHandler/module.js';
+
+/**
+ * @module ChristmasPromotionController
+ * 크리스마스 프로모션 이벤트 관련 애플리케이션의 흐름 제어를 담당
+ */
+const ChristmasPromotionController = {
+  /**
+   * @returns {Promise<void>}
+   */
+  async play() {
+    const { visitDate, orderMenuInfo } = await processUserInput();
+
+    processPromotionResult({ visitDate, orderMenuInfo });
+  },
+};
+
+/**
+ * @returns {Promise<{visitDate : number, orderMenuInfo : [string, number][]}>} 유저가 입력한 방문 일자 및 메뉴 정보를 반환할 Promise 객체
+ */
+async function processUserInput() {
+  OutputView.printStartGuideComments();
+
+  const visitDate = await ErrorHandler.retryOnErrors(InputView.readVisitDate.bind(InputView));
+  const orderMenuInfo = await ErrorHandler.retryOnErrors(
+    InputView.readOrderMenuInfo.bind(InputView),
+  );
+
+  return { visitDate, orderMenuInfo };
+}
+
+/**
+ * @param {{visitDate : number, orderMenuInfo : [string, number][]}} params - 유저가 입력한 방문 일자 및 메뉴 정보
+ * @returns {void}
+ */
+function processPromotionResult({ visitDate, orderMenuInfo }) {
+  const promotionResult = PromotionResultService.createPromotionResult({
+    visitDate,
+    orderMenuInfo,
+  });
+
+  OutputView.printEndGuideComments(visitDate);
+  OutputView.printPromotionResult({ orderMenuInfo, promotionResult });
+}
+
+export default ChristmasPromotionController;

리뷰: 컨트롤러 모듈이 짧고 읽기 편한 것 같아요!
@@ -0,0 +1,31 @@
+import { PROMOTION_MENU_TABLE } from '../../constants/promotionSystem.js';
+
+/**
+ * @module MenuSearcher
+ * 메뉴 테이블에 존재하는 메뉴를 찾기 위한 모듈
+ */
+const MenuSearcher = Object.freeze({
+  /**
+   * @param {string} menuName - 메뉴 이름
+   * @param {string} category - 메뉴 카테고리
+   * @returns {undefined | string} 매칭 되거나 매칭 되지 않은(undefined) 메뉴 이름
+   */
+  findByMenuName(menuName, category) {
+    const menusForSearch =
+      PROMOTION_MENU_TABLE[category] ??
+      Object.values(PROMOTION_MENU_TABLE).flatMap((section) => section);
+
+    return menusForSearch.find((menu) => menu.name === menuName);
+  },
+
+  /**
+   * @param {string} menuName - 메뉴 이름
+   * @param {string} category - 메뉴 카테고리
+   * @returns {boolean} 카테고리에 있는 메뉴 이름이 실제 메뉴 테이블에 존재하는지에 대한 여부
+   */
+  isMenuInCategory(menuName, category) {
+    return this.findByMenuName(menuName, category) !== undefined;
+  },
+});
+
+export default MenuSearcher;

리뷰: 빌트인 메서드 활용👍👍👍
@@ -0,0 +1,53 @@
+import { PROMOTION_DATE_INFO } from '../../constants/promotionSystem.js';
+
+// constant.js는 테스트 코드 및 module.js에서만 사용
+export const INITIAL_PROMOTION_BENEFIT_RESULT = Object.freeze({
+  xmasBenefitAmount: 0,
+  weekDayBenefitAmount: 0,
+  weekendBenefitAmount: 0,
+  specialBenefitAmount: 0,
+  giftAmount: 0,
+});
+
+export const MINIMUM_TOTAL_ORDER_AMOUNT = 10000;
+
+export const BENEFIT_DATE_INFO = Object.freeze({
+  startDate: new Date(`${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-01`),
+  endDate: new Date(`${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-31`),
+  christmas: new Date(`${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-25`),
+});
+
+export const DAY_OF_BENEFIT_CONDITION = Object.freeze({
+  weekday: Object.freeze({
+    menuCategory: '디저트',
+    days: [0, 1, 2, 3, 4],
+  }),
+
+  weekend: Object.freeze({
+    menuCategory: '메인',
+    days: [5, 6],
+  }),
+});
+
+export const BENEFIT_AMOUNT_INFO = Object.freeze({
+  everyDay: 100,
+  christmas: 1000,
+  dayOfWeek: 2023,
+  special: 1000,
+});
+
+export const MINIMUM_ORDER_AMOUNT_FOR_GIFT = 120000;
+
+export const GIFT_INFO = {
+  menuCategory: '음료',
+  menuName: '샴페인',
+};
+
+export const SPECIAL_DATES = new Set([
+  `${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-03`,
+  `${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-10`,
+  `${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-17`,
+  `${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-24`,
+  `${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-25`,
+  `${PROMOTION_DATE_INFO.year}-${PROMOTION_DATE_INFO.month}-31`,
+]);

리뷰: 도메인마다 상수 파일을 만들어서 사용하고 계시는데, 이미 있는 `constants` 디렉토리에 상수를 묶어놓지 않으시는 이유가 있는지 궁금해요. 만약 새로운 개발자가 들어온다면, `constants`에 모든 상수가 들어 있을 거라고 기대할 것 같은데요. 중요한 상수들이 `domain` 안에 있는 하위 디렉토리에 들어있어서 혼란을 줄 수 있을 것 같습니다. 테스트 코드 파일도 마찬가지라고 생각해요😅.

상수나 테스트 파일 모두 특정 디렉토리 안에 묶으면 사용이나 관리가 더 수월할 것 같은데 어떻게 생각하시나요? 또 그렇게 하면 `domain`의 하위 디렉토리들을 없앨 수 있어서 `domain` 파일들도 관리하기가 수월해질 것 같아요.