ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,87 @@
+import VisitDate from '../domain/VisitDate.js';
+import Order from '../domain/Order.js';
+import InputView from '../views/InputView.js';
+import OutputView from '../views/OutputView.js';
+import EventPlanner from '../domain/EventPlanner.js';
+import Save from '../domain/Save.js';
+
+class EventController {
+  #inputView;
+
+  #outputView;
+
+  #dayIndex;
+
+  #eventPlanner;
+
+  constructor() {
+    this.#inputView = InputView;
+    this.#outputView = OutputView;
+  }
+
+  async startEvent() {
+    this.#outputView.printIntro();
+
+    const visitDate = await this.#requestVisitDate();
+    const orderList = await this.#requestOrderList();
+
+    this.#eventPlanner = this.#showEventPlanner(visitDate, orderList);
+    this.#saveResult(this.#eventPlanner);
+  }
+
+  /**
+   * 방문 날짜를 입력 받아 반환, 요일 인덱스 멤버 변수 할당
+   * @returns {number} - 요일 인덱스
+   */
+  async #requestVisitDate() {
+    try {
+      const visitDate = await this.#inputView.readDate();
+      this.#dayIndex = new VisitDate(visitDate).getDayIndex();
+
+      return visitDate;
+    } catch ({ message }) {
+      this.#outputView.print(message);
+
+      return this.#requestVisitDate();
+    }
+  }
+
+  /**
+   * 주문할 메뉴와 개수를 입력 받아 가공한 주문 메뉴를 리턴
+   * @returns {Set<{ menu: string, quantity: number }>}
+   */
+  async #requestOrderList() {
+    try {
+      const orderInput = await this.#inputView.readOrder();
+
+      return new Order(orderInput).getOrderList();
+    } catch ({ message }) {
+      this.#outputView.print(message);
+
+      return this.#requestOrderList();
+    }
+  }
+
+  /**
+   * 유저의 입력 값을 기반으로 이벤트 플래너를 구성하고 출력
+   * @param {number} visitDate - 요일 인덱스
+   * @param {Set<{ menu: string, quantity: number }>} orderList - 주문 메뉴와 수량을 나타내는 Set 객체
+   * @returns
+   */
+  #showEventPlanner(visitDate, orderList) {
+    this.#outputView.printOutro(visitDate);
+    this.#eventPlanner = new EventPlanner(visitDate, this.#dayIndex, orderList);
+    this.#outputView.printPlanner(orderList, this.#eventPlanner);
+
+    return this.#eventPlanner;
+  }
+
+  /**
+   * 이벤트 플래너의 주요 데이터를 Save 클래스에 넘겨 저장
+   */
+  #saveResult() {
+    new Save(this.#eventPlanner);
+  }
+}
+
+export default EventController;

리뷰: 에러메시지 출력 기능을 `outputView`가 책임을 맡도록 했군요! 리팩토링 할 때 저도 반영해봐야겠습니다 👍
@@ -0,0 +1,32 @@
+import { ERROR_MESSAGES } from '../constants/messages.js';
+import { TIME } from '../constants/system.js';
+import validationErrorHandler from '../errors/index.js';
+import { isPositiveInteger, isValidDate } from '../validators/index.js';
+
+class VisitDate {
+  #visitDate;
+
+  constructor(date) {
+    this.#validateDate(date);
+    this.#visitDate = date;
+  }
+
+  #validateDate(date) {
+    if (!isPositiveInteger(date) || !isValidDate(date)) {
+      validationErrorHandler(ERROR_MESSAGES.invalidDate);
+    }
+  }
+
+  getDayIndex() {
+    return this.#calculateDayIndex();
+  }
+
+  // 이벤트 연-월의 방문 날짜를 계산하여 요일 인덱스를 반환
+  #calculateDayIndex() {
+    const date = new Date(`${TIME.dateFormat}${this.#visitDate}`);
+
+    return date.getDay();
+  }
+}
+
+export default VisitDate;

리뷰: 기본 제공되는 `Date` 객체를 활용할 생각은 못했네요!
이렇게 신박한 방법도 있다는 것을 알아갑니다 👍
@@ -0,0 +1,9 @@
+import { ERROR_MESSAGES } from '../constants/messages.js';
+
+class AppError extends Error {
+  constructor(message) {
+    super(`${ERROR_MESSAGES.prefix} ${message}`);
+  }
+}
+
+export default AppError;

리뷰: 에러를 상속하셨군요! 정말 체계적인 코드인 것 같아요!
`Error`를 상속해 `AppError` 클래스를 따로 만드시게 된 계기를 여쭤봐도 될까요?
@@ -0,0 +1,12 @@
+import MENUS from '../constants/menus.js';
+
+/**
+ * 메뉴 이름을 입력하면 카테고리 내 메뉴의 가격 값을 반환
+ * @param {string} menu - 메뉴 이름
+ * @returns {number | undefined}
+ */
+const menuPriceFinder = menu => {
+  return Object.values(MENUS).find(category => menu in category)[menu];
+};
+
+export default menuPriceFinder;

리뷰: 기능을 구현하다보면 메뉴의 가격을 자주 가져다 쓰게 되는데 utils 폴더에 따로 함수를 구현해두니 좋은 것 같아요!
배워갑니다 👏
@@ -0,0 +1,15 @@
+import { UNITS } from '../constants/system.js';
+
+/**
+ * number type의 가격을 입력받아 가격을 변환 후 '원'을 포함한 string type으로 변환
+ * 20000이라는 매개변수를 통해 '20,000원'을 반환
+ * @param {number} price 가격
+ * @returns {string}
+ */
+const priceFormatter = price => {
+  const formattedPrice = new Intl.NumberFormat().format(price);
+
+  return `${formattedPrice}${UNITS.won}`;
+};
+
+export default priceFormatter;

리뷰: `Intl.NumberFormat` 를 처음 접했는데 이렇게 가격을 포맷하는 간단한 방법이 있었군요!
저는 정규표현식으로 구현했는데 새로운 방법을 알아가게 돼서 좋아요!
@@ -0,0 +1,68 @@
+import { ERROR_MESSAGES } from '../constants/messages.js';
+import { SYMBOLS } from '../constants/system.js';
+import validationErrorHandler from '../errors/index.js';
+import { isValidMenuName } from '../validators/index.js';
+import { isOnlyBeverage } from '../validators/is-valid-menu/name.js';
+import {
+  isValidEachMenuQuantity,
+  isValidTotalMenuQuantity,
+} from '../validators/is-valid-menu/quantity.js';
+
+class Order {
+  #orderList = new Set();
+
+  constructor(orderInput) {
+    this.#validate(orderInput);
+    this.#orderList = this.#parse(orderInput);
+  }
+
+  /**
+   * 사용자 입력한 주문 메뉴와 메뉴 개수를 파싱하여 배열로 반환
+   * @param {string} orderInput - 사용자가 입력한 모든 주문 메뉴와 메뉴 개수
+   * @returns {{ key: string, value: string }[]} - 주문이 파싱된 배열
+   */
+  #parse(orderInput) {
+    this.#splitOrder(orderInput);
+
+    return [...this.#orderList];
+  }
+
+  /**
+   * orderInput 내 개별 주문들을 파싱
+   * @param {string} orderInput - 사용자가 입력한 모든 주문 메뉴와 메뉴 개수
+   * @returns {Set<{ menu: string, quantity: number }>} - 주문이 파싱된 Set
+   */
+  #splitOrder(orderInput) {
+    return orderInput.split(SYMBOLS.comma).map(order => this.#splitMenu(order));
+  }
+
+  /**
+   * 주문 메뉴명과 개수를 orderList Set에 추가
+   * @param {string} order 사용자가 입력한 특정 주문 메뉴와 메뉴 개수
+   */
+  #splitMenu(order) {
+    const [menu, quantity] = order.split(SYMBOLS.dash);
+
+    this.#orderList.add({ menu: menu.trim(), quantity: Number(quantity) });
+  }
+
+  #validate(orderInput) {
+    if (!isValidMenuName(orderInput) || !isValidEachMenuQuantity(orderInput)) {
+      validationErrorHandler(ERROR_MESSAGES.invalidMenu);
+    }
+
+    if (!isValidTotalMenuQuantity(orderInput)) {
+      validationErrorHandler(ERROR_MESSAGES.exceedQuantity);
+    }
+
+    if (isOnlyBeverage(orderInput)) {
+      validationErrorHandler(ERROR_MESSAGES.onlyBeverage);
+    }
+  }
+
+  getOrderList() {
+    return this.#orderList;
+  }
+}
+
+export default Order;

리뷰: 메소드 순서를 잘 정하셔서 이해하기 정말 좋았습니다!
어, 이 메소드는 뭐지? 싶으면 바로 밑에 그 메소드가 있어서 술술 읽혔어요!
메소드 순서도 컨벤션이라고 하는 공통 피드백이 있었는데 그걸 잘 반영하신 것 같아요 👍
@@ -0,0 +1,57 @@
+import { GIVEAWAYS, TITLES } from '../constants/events.js';
+import { NONE } from '../constants/system.js';
+import menuPriceFinder from '../utils/menuPriceFinder.js';
+
+class Benefit {
+  #isFitGiveaway;
+
+  #benefitList;
+
+  #totalPrice;
+
+  constructor(discountList, isFitGiveaway) {
+    this.#isFitGiveaway = isFitGiveaway;
+    this.#benefitList = this.#setBenefitList(discountList);
+    this.#totalPrice = this.#calculateTotalPrice();
+  }
+
+  /**
+   * 증정 이벤트 기준 이상이면 혜택 내역에 증정 메뉴를 추가하고, 미만이면 '없음'을 추가
+   * @param {Array<{ title: string, price: number }>} benefitList - 혜택 리스트
+   * @returns
+   */
+  #setBenefitList(discountList) {
+    if (this.#isFitGiveaway) {
+      return [...discountList, this.#createGiveawayBenefit()];
+    }
+
+    return discountList;
+  }
+
+  #createGiveawayBenefit() {
+    return {
+      title: TITLES.giveaway,
+      price: menuPriceFinder(GIVEAWAYS.giveaway),
+    };
+  }
+
+  /**
+   * 혜택 내역이 '없음'이면 총 금액 0을 반환, 혜택 내역이 있으면 총 금액을 누산하여 반환
+   * @returns
+   */
+  #calculateTotalPrice() {
+    if (this.#benefitList === NONE) return 0;
+
+    return this.#benefitList.reduce((acc, benefit) => acc + benefit.price, 0);
+  }
+
+  getList() {
+    return this.#benefitList;
+  }
+
+  getTotalPrice() {
+    return this.#totalPrice;
+  }
+}
+
+export default Benefit;

리뷰: 3주차 공통 피드백에 `필드의 수를 줄이기 위해 노력한다`라는 말이 있었죠!
`#benefitList`와 `#totalPrice`는
생성자의 인수로 전달된 `discountList`, `isFitGiveaway`으로 만들 수 있기 때문에
꼭 필드로 선언해야 하는 건지에 대한 의문이 생깁니다!
필드의 개수를 최대한 줄이는 게 정말 옳은 것인지 고민이 많아지네요
희태님 생각은 어떠신가요?!
@@ -0,0 +1,106 @@
+import { BADGES, GIVEAWAYS } from '../constants/events.js';
+import { NONE } from '../constants/system.js';
+import menuPriceFinder from '../utils/menuPriceFinder.js';
+import Benefit from './Benefit.js';
+import Discount from './Discount.js';
+
+class EventPlanner {
+  #benefit;
+
+  #preTotalPrice;
+
+  #totalBenefitPrice;
+
+  constructor(visitDate, dayIndex, orderList) {
+    this.discount = new Discount(visitDate, dayIndex, orderList);
+    this.#preTotalPrice = this.#setPreTotalPrice(orderList);
+    this.isFitGiveaway = this.#checkIsFitGiveAway();
+    this.#benefit = new Benefit(
+      this.discount.getDiscounts(this.#preTotalPrice),
+      this.isFitGiveaway,
+    );
+  }
+
+  /**
+   * 주문 내역을 순회하며 할인 전 총주문 금액을 반환
+   * @param {Array<{ menu: string, quantity: number }>} orderList - 주문 내역
+   * @returns
+   */
+  #setPreTotalPrice(orderList) {
+    return orderList.reduce(
+      (acc, order) => this.#calculateTotalPrice(acc, order),
+      0,
+    );
+  }
+
+  /**
+   * 특정 메뉴의 가격과 수량을 곱하여 할인 전 총주문 금액을 계산
+   * @param {number} acc - 누적 금액
+   * @param {{ menu: string, quantity: number }} order - 특정 주문 내역
+   * @returns
+   */
+  #calculateTotalPrice(acc, order) {
+    const price = menuPriceFinder(order.menu);
+
+    return acc + price * order.quantity;
+  }
+
+  getPreTotalPrice() {
+    return this.#preTotalPrice;
+  }
+
+  // 할인 전 총주문 금액과 증정 이벤트 기준 금액을 비교하여 boolean 반환
+  #checkIsFitGiveAway() {
+    return this.#preTotalPrice >= GIVEAWAYS.giveawayPrice;
+  }
+
+  // 증정 이벤트 기준 금액 이상이면 증정 이벤트 상품 반환, 미만이면 '없음' 반환
+  getGiveaway() {
+    return this.isFitGiveaway ? [this.#createGiveaway()] : NONE;
+  }
+
+  #createGiveaway() {
+    return { menu: GIVEAWAYS.giveaway, quantity: GIVEAWAYS.giveawayUnit };
+  }
+
+  getBenefitList() {
+    const benefitList = this.#benefit.getList();
+
+    return benefitList.length ? benefitList : NONE;
+  }
+
+  getTotalBenefitPrice() {
+    this.#totalBenefitPrice = this.#benefit.getTotalPrice();
+
+    return this.#totalBenefitPrice;
+  }
+
+  /**
+   * 총 할인 금액이 존재하면 할인 후 예상 결제 금액 반환, 그렇지 않다면 할인 전 총주문 금액 반환
+   * @returns
+   */
+  getTotalPrice() {
+    const totalDiscountPrice = this.discount.getTotalDiscountPrice();
+
+    return this.#preTotalPrice - totalDiscountPrice;
+  }
+
+  /**
+   * 총혜택 금액에 따른 이벤트 배지 반환
+   * @returns
+   */
+  getBadge() {
+    switch (true) {
+      case this.#totalBenefitPrice > BADGES.santa.price:
+        return BADGES.santa.title;
+      case this.#totalBenefitPrice > BADGES.tree.price:
+        return BADGES.tree.title;
+      case this.#totalBenefitPrice > BADGES.star.price:
+        return BADGES.star.title;
+      default:
+        return NONE;
+    }
+  }
+}
+
+export default EventPlanner;

리뷰: `switch`문을 사용하니 정말 깔끔한 것 같아요!
@@ -0,0 +1,43 @@
+import fs from 'fs';
+import { FILE_PATH } from '../constants/system.js';
+
+class Save {
+  #eventPlanner;
+
+  #filePath;
+
+  constructor(eventPlanner) {
+    this.#eventPlanner = eventPlanner;
+    this.#filePath = FILE_PATH;
+    this.#writeData();
+  }
+
+  // 기존 JSON 데이터에 신규 데이터를 추가하고 저장
+  #writeData() {
+    const readData = this.#readData();
+    const userData = {
+      user: readData.length,
+      badge: this.#eventPlanner.getBadge(),
+      total: this.#eventPlanner.getPreTotalPrice(),
+      benefit: this.#eventPlanner.getTotalBenefitPrice(),
+      payment: this.#eventPlanner.getTotalPrice(),
+    };
+    readData.push(userData);
+    const jsonData = JSON.stringify(readData, null, 2);
+
+    fs.writeFileSync(this.#filePath, jsonData, 'utf-8');
+  }
+
+  // 데이터를 읽어와 파싱하여 반환, 기존의 데이터가 없다면 빈 배열 추가
+  #readData() {
+    try {
+      const data = fs.readFileSync(this.#filePath, 'utf-8');
+
+      return JSON.parse(data);
+    } catch (error) {
+      return [];
+    }
+  }
+}
+
+export default Save;

리뷰: 방문한 손님들에 대한 데이터를 저장하는 기능은 생각 못 했는데
너무 좋은 아이디어입니다 ✨
@@ -0,0 +1,21 @@
+import { EOL as LINE_SEPARATOR } from 'os';
+import { TIME, UNITS } from './system.js';
+
+export const OUTPUT_MESSAGES = Object.freeze({
+  intro: `안녕하세요! 우테코 식당 ${TIME.month}${UNITS.month} 이벤트 플래너입니다.`,
+  outro: `일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!`,
+});
+
+export const INPUT_MESSAGES = Object.freeze({
+  date: `${TIME.month}${UNITS.month} 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)${LINE_SEPARATOR}`,
+  order: `주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)${LINE_SEPARATOR}`,
+});
+
+export const ERROR_MESSAGES = Object.freeze({
+  prefix: '[ERROR]',
+  invalidDate: '유효하지 않은 날짜입니다. 다시 입력해 주세요.',
+  invalidMenu: '유효하지 않은 주문입니다. 다시 입력해 주세요.',
+  exceedQuantity:
+    '메뉴는 한 번에 최대 20개까지만 주문할 수 있습니다. 다시 입력해 주세요.',
+  onlyBeverage: '음료만 주문할 수 없습니다. 다시 입력해 주세요.',
+});

리뷰: 이번 미션에서는 확장성을 신경쓰고자 노력했는데 알아봐주셔서 감사할 따름입니다! 🙂
@@ -0,0 +1,32 @@
+import { ERROR_MESSAGES } from '../constants/messages.js';
+import { TIME } from '../constants/system.js';
+import validationErrorHandler from '../errors/index.js';
+import { isPositiveInteger, isValidDate } from '../validators/index.js';
+
+class VisitDate {
+  #visitDate;
+
+  constructor(date) {
+    this.#validateDate(date);
+    this.#visitDate = date;
+  }
+
+  #validateDate(date) {
+    if (!isPositiveInteger(date) || !isValidDate(date)) {
+      validationErrorHandler(ERROR_MESSAGES.invalidDate);
+    }
+  }
+
+  getDayIndex() {
+    return this.#calculateDayIndex();
+  }
+
+  // 이벤트 연-월의 방문 날짜를 계산하여 요일 인덱스를 반환
+  #calculateDayIndex() {
+    const date = new Date(`${TIME.dateFormat}${this.#visitDate}`);
+
+    return date.getDay();
+  }
+}
+
+export default VisitDate;

리뷰: 아마 더 좋은 방법이 있을겁니다! :) 저도 `Date` 객체를 잘 활용하지 못한 것 같아서 더 공부해보려구요!
@@ -0,0 +1,9 @@
+import { ERROR_MESSAGES } from '../constants/messages.js';
+
+class AppError extends Error {
+  constructor(message) {
+    super(`${ERROR_MESSAGES.prefix} ${message}`);
+  }
+}
+
+export default AppError;

리뷰: 아무래도 `[ERROR]` 라는 prefix를 통해 에러 메시지를 정의하는 과정에서 만들게 되었는데요, 지정 에러 클래스를 사용하면 추후에 더 쉽게 확장할 수 있는 장점이 있지 않을까라는 생각이었습니다!
@@ -0,0 +1,15 @@
+import { UNITS } from '../constants/system.js';
+
+/**
+ * number type의 가격을 입력받아 가격을 변환 후 '원'을 포함한 string type으로 변환
+ * 20000이라는 매개변수를 통해 '20,000원'을 반환
+ * @param {number} price 가격
+ * @returns {string}
+ */
+const priceFormatter = price => {
+  const formattedPrice = new Intl.NumberFormat().format(price);
+
+  return `${formattedPrice}${UNITS.won}`;
+};
+
+export default priceFormatter;

리뷰: `Intl.NumberFormat` 생성자를 통해 다양한 통화 서식을 변환할 수 있더라구요!

[Intl.NumberFormat](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat)

도움이 될까싶어 링크 첨부해두었습니다 :)
@@ -0,0 +1,57 @@
+import { GIVEAWAYS, TITLES } from '../constants/events.js';
+import { NONE } from '../constants/system.js';
+import menuPriceFinder from '../utils/menuPriceFinder.js';
+
+class Benefit {
+  #isFitGiveaway;
+
+  #benefitList;
+
+  #totalPrice;
+
+  constructor(discountList, isFitGiveaway) {
+    this.#isFitGiveaway = isFitGiveaway;
+    this.#benefitList = this.#setBenefitList(discountList);
+    this.#totalPrice = this.#calculateTotalPrice();
+  }
+
+  /**
+   * 증정 이벤트 기준 이상이면 혜택 내역에 증정 메뉴를 추가하고, 미만이면 '없음'을 추가
+   * @param {Array<{ title: string, price: number }>} benefitList - 혜택 리스트
+   * @returns
+   */
+  #setBenefitList(discountList) {
+    if (this.#isFitGiveaway) {
+      return [...discountList, this.#createGiveawayBenefit()];
+    }
+
+    return discountList;
+  }
+
+  #createGiveawayBenefit() {
+    return {
+      title: TITLES.giveaway,
+      price: menuPriceFinder(GIVEAWAYS.giveaway),
+    };
+  }
+
+  /**
+   * 혜택 내역이 '없음'이면 총 금액 0을 반환, 혜택 내역이 있으면 총 금액을 누산하여 반환
+   * @returns
+   */
+  #calculateTotalPrice() {
+    if (this.#benefitList === NONE) return 0;
+
+    return this.#benefitList.reduce((acc, benefit) => acc + benefit.price, 0);
+  }
+
+  getList() {
+    return this.#benefitList;
+  }
+
+  getTotalPrice() {
+    return this.#totalPrice;
+  }
+}
+
+export default Benefit;

리뷰: 저는 우선 모든 메서드와 필드를 `private`으로 지정하고나서, 이후에 `public`으로 접근을 해야만 할때 `public`으로 전환하였습니다! 
아마 이 과정에서도 클래스 내 메서드가 `benefitList`와 `totalPrice`를 사용함에 있어서 `public`으로 전환하지 않아도 될 것 같아서 그대로 두었습니다 :)

저도 사실 공통 피드백을 받고나서 필드의 개수를 최대한 줄여야하는 것과, 최대한 private 필드로 캡슐화를 구현하는 것 사이에서의 중간점을 못찾겠더라구요! 

좋은 피드백 감사드립니다! `benefitList`와 `totalPrice`를 굳이 private 필드로 선언해야하는 근거가 없으면 줄이는것도 맞다고 생각이 듭니다.
@@ -0,0 +1,68 @@
+import { ERROR_MESSAGES } from '../constants/messages.js';
+import { SYMBOLS } from '../constants/system.js';
+import validationErrorHandler from '../errors/index.js';
+import { isValidMenuName } from '../validators/index.js';
+import { isOnlyBeverage } from '../validators/is-valid-menu/name.js';
+import {
+  isValidEachMenuQuantity,
+  isValidTotalMenuQuantity,
+} from '../validators/is-valid-menu/quantity.js';
+
+class Order {
+  #orderList = new Set();
+
+  constructor(orderInput) {
+    this.#validate(orderInput);
+    this.#orderList = this.#parse(orderInput);
+  }
+
+  /**
+   * 사용자 입력한 주문 메뉴와 메뉴 개수를 파싱하여 배열로 반환
+   * @param {string} orderInput - 사용자가 입력한 모든 주문 메뉴와 메뉴 개수
+   * @returns {{ key: string, value: string }[]} - 주문이 파싱된 배열
+   */
+  #parse(orderInput) {
+    this.#splitOrder(orderInput);
+
+    return [...this.#orderList];
+  }
+
+  /**
+   * orderInput 내 개별 주문들을 파싱
+   * @param {string} orderInput - 사용자가 입력한 모든 주문 메뉴와 메뉴 개수
+   * @returns {Set<{ menu: string, quantity: number }>} - 주문이 파싱된 Set
+   */
+  #splitOrder(orderInput) {
+    return orderInput.split(SYMBOLS.comma).map(order => this.#splitMenu(order));
+  }
+
+  /**
+   * 주문 메뉴명과 개수를 orderList Set에 추가
+   * @param {string} order 사용자가 입력한 특정 주문 메뉴와 메뉴 개수
+   */
+  #splitMenu(order) {
+    const [menu, quantity] = order.split(SYMBOLS.dash);
+
+    this.#orderList.add({ menu: menu.trim(), quantity: Number(quantity) });
+  }
+
+  #validate(orderInput) {
+    if (!isValidMenuName(orderInput) || !isValidEachMenuQuantity(orderInput)) {
+      validationErrorHandler(ERROR_MESSAGES.invalidMenu);
+    }
+
+    if (!isValidTotalMenuQuantity(orderInput)) {
+      validationErrorHandler(ERROR_MESSAGES.exceedQuantity);
+    }
+
+    if (isOnlyBeverage(orderInput)) {
+      validationErrorHandler(ERROR_MESSAGES.onlyBeverage);
+    }
+  }
+
+  getOrderList() {
+    return this.#orderList;
+  }
+}
+
+export default Order;

리뷰: 제 코드는 스스로 다소 난잡한 코드라고 생각이 드는데, 다행입니다 하하
@@ -0,0 +1,15 @@
+import { UNITS } from '../constants/system.js';
+
+/**
+ * number type의 가격을 입력받아 가격을 변환 후 '원'을 포함한 string type으로 변환
+ * 20000이라는 매개변수를 통해 '20,000원'을 반환
+ * @param {number} price 가격
+ * @returns {string}
+ */
+const priceFormatter = price => {
+  const formattedPrice = new Intl.NumberFormat().format(price);
+
+  return `${formattedPrice}${UNITS.won}`;
+};
+
+export default priceFormatter;

리뷰: 저는 이 부분을 [Number.toLocaleString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString)을 적용했는데, 숫자 형식과 관련된 부분은 Intl 객체를 사용하는게 더 정확할 것 같네요 👍 (내부적으로 같은 메서드를 사용하는 거 같아요 ㅎㅎ)
@@ -0,0 +1,24 @@
+const MENUS = Object.freeze({
+  appetizer: {
+    양송이수프: 6_000,
+    타파스: 5_500,
+    시저샐러드: 8_000,
+  },
+  main: {
+    티본스테이크: 55_000,
+    바비큐립: 54_000,
+    해산물파스타: 35_000,
+    크리스마스파스타: 25_000,
+  },
+  dessert: {
+    초코케이크: 15_000,
+    아이스크림: 5_000,
+  },
+  beverage: {
+    제로콜라: 3_000,
+    레드와인: 60_000,
+    샴페인: 25_000,
+  },
+});
+
+export default MENUS;

리뷰: 혹시 menu도 data 폴더에 넣는 방향도 생각해 보셨을까요? 저 같은 경우엔 상수로 묶기엔 걸리는 부분들이 있었던 것 같아서요! 개인적으로 궁금해서 여쭤봅니다! 🙂
@@ -1,5 +1,15 @@
+import EventController from './controller/index.js';
+
 class App {
-  async run() {}
+  #controller;
+
+  constructor() {
+    this.#controller = new EventController();
+  }
+
+  async run() {
+    await this.#controller.startEvent();
+  }
 }
 
 export default App;

리뷰: App이 정말 깔끔하네요..! 뒤에 숨겨진 코드들 보고 많이 배워갑니다! 🏃‍♂️
@@ -0,0 +1,52 @@
+import { PROMOTION_TITLES, TIME, UNITS } from './system.js';
+
+export const DISCOUNT_PRICES = Object.freeze({
+  dDay: 1_000,
+  dDayInc: 100,
+  week: 2_023,
+  special: 1_000,
+});
+
+export const BADGES = Object.freeze({
+  star: { title: '별', price: 5_000 },
+  tree: { title: '트리', price: 10_000 },
+  santa: { title: '산타', price: 20_000 },
+});
+
+export const TITLES = Object.freeze({
+  dDay: `${PROMOTION_TITLES[TIME.month]} 디데이 할인`,
+  weekday: '평일 할인',
+  weekend: '주말 할인',
+  special: '특별 할인',
+  giveaway: '증정 이벤트',
+});
+
+export const GIVEAWAYS = Object.freeze({
+  giveaway: '샴페인',
+  giveawayUnit: 1,
+  giveawayPrice: 120_000,
+});
+
+export const ORDER = Object.freeze({
+  minPrice: 10_000,
+  minQuantity: 1,
+  maxQuantity: 20,
+});
+
+export const DATES = Object.freeze({
+  startDate: 1,
+  endDate: 31,
+  christmas: 25,
+  specialDayIndex: 0,
+  weekendIndex: 5,
+});
+
+export const BENEFITS = Object.freeze({
+  menu: '<주문 메뉴>',
+  preTotalPrice: '<할인 전 총주문 금액>',
+  giveaway: '<증정 메뉴>',
+  benefitList: '<혜택 내역>',
+  totalBenefitPrice: '<총혜택 금액>',
+  totalPrice: '<할인 후 예상 결제 금액>',
+  badge: `<${TIME.month}${UNITS.month} 이벤트 배지>`,
+});

리뷰: `dDayInc`보다는 좀더 구체적인 상수명은 어떨까요?
@@ -0,0 +1,52 @@
+import { PROMOTION_TITLES, TIME, UNITS } from './system.js';
+
+export const DISCOUNT_PRICES = Object.freeze({
+  dDay: 1_000,
+  dDayInc: 100,
+  week: 2_023,
+  special: 1_000,
+});
+
+export const BADGES = Object.freeze({
+  star: { title: '별', price: 5_000 },
+  tree: { title: '트리', price: 10_000 },
+  santa: { title: '산타', price: 20_000 },
+});
+
+export const TITLES = Object.freeze({
+  dDay: `${PROMOTION_TITLES[TIME.month]} 디데이 할인`,
+  weekday: '평일 할인',
+  weekend: '주말 할인',
+  special: '특별 할인',
+  giveaway: '증정 이벤트',
+});
+
+export const GIVEAWAYS = Object.freeze({
+  giveaway: '샴페인',
+  giveawayUnit: 1,
+  giveawayPrice: 120_000,
+});
+
+export const ORDER = Object.freeze({
+  minPrice: 10_000,
+  minQuantity: 1,
+  maxQuantity: 20,
+});
+
+export const DATES = Object.freeze({
+  startDate: 1,
+  endDate: 31,
+  christmas: 25,
+  specialDayIndex: 0,
+  weekendIndex: 5,
+});
+
+export const BENEFITS = Object.freeze({
+  menu: '<주문 메뉴>',
+  preTotalPrice: '<할인 전 총주문 금액>',
+  giveaway: '<증정 메뉴>',
+  benefitList: '<혜택 내역>',
+  totalBenefitPrice: '<총혜택 금액>',
+  totalPrice: '<할인 후 예상 결제 금액>',
+  badge: `<${TIME.month}${UNITS.month} 이벤트 배지>`,
+});

리뷰: 새해 이벤트까지는 생각안해봤는데 꼼꼼하십니다!👍
@@ -0,0 +1,15 @@
+import { UNITS } from '../constants/system.js';
+
+/**
+ * number type의 가격을 입력받아 가격을 변환 후 '원'을 포함한 string type으로 변환
+ * 20000이라는 매개변수를 통해 '20,000원'을 반환
+ * @param {number} price 가격
+ * @returns {string}
+ */
+const priceFormatter = price => {
+  const formattedPrice = new Intl.NumberFormat().format(price);
+
+  return `${formattedPrice}${UNITS.won}`;
+};
+
+export default priceFormatter;

리뷰: 맞습니다! 석표님 말씀처럼 결국 `toLocaleString()`이 궁극적으로 `Intl.NumberFormat` API를 사용하더라구요 :)

어떻게보면 좀더 손쉬운 사용을 위한 `toLocaleString()` 메서드 사용이 더 목적에 부합할 수도 있다고 생각이 드네요 :)
@@ -0,0 +1,14 @@
+package step1;
+
+public class Print {
+
+    private int calculateResult;
+
+    public Print(int calculateResult) {
+        this.calculateResult = calculateResult;
+    }
+
+    public void printResult() {
+        System.out.println("calculateResult = " + calculateResult);
+    }
+}

리뷰: Print가 하나의 계산결과를 상태로 가지도록 구현하셨네요!
만약 Print를 통해 또 다른 계산 결과를 출력해야한다면 어떻게 될까요? Print라는 클래스의 책임에 맞는 역할을 하고 있다고 볼 수 있을까요?