ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,87 @@
+import { SETTING } from '../constant/index.js';
+import { MenuRepository } from '../repository/index.js';
+import {
+  MenuAmountError,
+  MenuOnlyBeverageError,
+} from '../error/CustomError.js';
+
+export default class Menus {
+  #menus;
+
+  constructor(menus) {
+    this.#menus = new Map();
+
+    Object.keys(menus).forEach(key => {
+      this.#menus.set(MenuRepository.getMenuByName(key), menus[key]);
+    });
+
+    this.#validateAmounts();
+    this.#validateOnlyBeverage();
+  }
+
+  #validateAmounts() {
+    let amount = 0;
+    this.#menus.forEach(value => {
+      amount += value;
+    });
+
+    if (amount > SETTING.maximumMenusAmount) {
+      throw new MenuAmountError();
+    }
+  }
+
+  #validateOnlyBeverage() {
+    if (this.#onlyBeverage()) {
+      throw new MenuOnlyBeverageError();
+    }
+  }
+
+  list() {
+    const string = [];
+
+    this.#menus.forEach((value, key) => {
+      string.push(`${key.get().name} ${value}개`);
+    });
+
+    return string;
+  }
+
+  previousPrice() {
+    let price = 0;
+    this.#menus.forEach((value, key) => {
+      price += key.get().price * value;
+    });
+
+    return price;
+  }
+
+  canApplyEvent() {
+    if (this.previousPrice() > SETTING.minimumApplyEventPrice) {
+      return true;
+    }
+    return false;
+  }
+
+  #onlyBeverage() {
+    let notBeverage = 0;
+    this.#menus.forEach((value, key) => {
+      if (key.get().type !== 'beverage') {
+        notBeverage += value;
+      }
+    });
+
+    if (notBeverage === 0) {
+      return true;
+    }
+    return false;
+  }
+
+  types() {
+    const types = { appetizer: 0, main: 0, beverage: 0, dessert: 0 };
+    this.#menus.forEach((value, key) => {
+      types[key.get().type] += value;
+    });
+
+    return types;
+  }
+}

리뷰: `이벤트의 실행을 결정` 하는 책임은 `Events`가 가지고 있을 수도 있을 것 같아요!
@@ -0,0 +1,76 @@
+import ERROR from '../constant/Error.js';
+
+export class DateTypeError extends Error {
+  constructor() {
+    super(ERROR.dateType);
+  }
+}
+
+export class MenuOrderTypeError extends Error {
+  constructor() {
+    super(ERROR.menuOrderType);
+  }
+}
+
+export class MenuDuplicatedError extends Error {
+  constructor() {
+    super(ERROR.menuDuplicated);
+  }
+}
+
+export class MenuNotExistError extends Error {
+  constructor() {
+    super(ERROR.menuNotExist);
+  }
+}
+
+export class MenuAmountError extends Error {
+  constructor() {
+    super(ERROR.menuAmount);
+  }
+}
+
+export class MenuOnlyBeverageError extends Error {
+  constructor() {
+    super(ERROR.menuOnlyBeverage);
+  }
+}
+
+export class MenuNameError extends Error {
+  constructor() {
+    super(ERROR.menuName);
+  }
+}
+
+export class MenuTypeError extends Error {
+  constructor() {
+    super(ERROR.menuType);
+  }
+}
+export class MenuPriceError extends Error {
+  constructor() {
+    super(ERROR.menuPrice);
+  }
+}
+
+export class EventTypeError extends Error {
+  constructor() {
+    super(ERROR.eventType);
+  }
+}
+export class EventStatusError extends Error {
+  constructor() {
+    super(ERROR.eventStatus);
+  }
+}
+export class EventAmountError extends Error {
+  constructor() {
+    super(ERROR.eventAmount);
+  }
+}
+
+export class EventNotExistError extends Error {
+  constructor() {
+    super(ERROR.eventNotExist);
+  }
+}

리뷰: 커스텀에러 👏
@@ -0,0 +1,33 @@
+const SETTING = Object.freeze({
+  date: {
+    minimun: 1,
+    maximum: 31,
+    christmas: 25,
+    friday: 1,
+    saturday: 2,
+    sunday: 3,
+  },
+  weekDays: 7,
+
+  locale: 'ko-KR',
+  menuSplit: ',',
+  menuAmountSplit: '-',
+
+  maximumMenusAmount: 20,
+  minimumApplyEventPrice: 10000,
+
+  christmasDiscount: { default: -1000, forDay: -100 },
+  weekDiscount: -2023,
+  specialDiscount: -1000,
+
+  minimumPresentPrice: 120000,
+  presentMenu: '샴페인',
+
+  minimumAmountBadge: {
+    santa: -20000,
+    tree: -10000,
+    star: -5000,
+  },
+});
+
+export default SETTING;

리뷰: 휴먼에러가 있네요! [익스텐션](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)을 사용해보는건 어떨까요?
@@ -0,0 +1,87 @@
+import { SETTING } from '../constant/index.js';
+import { MenuRepository } from '../repository/index.js';
+import {
+  MenuAmountError,
+  MenuOnlyBeverageError,
+} from '../error/CustomError.js';
+
+export default class Menus {
+  #menus;
+
+  constructor(menus) {
+    this.#menus = new Map();
+
+    Object.keys(menus).forEach(key => {
+      this.#menus.set(MenuRepository.getMenuByName(key), menus[key]);
+    });
+
+    this.#validateAmounts();
+    this.#validateOnlyBeverage();
+  }
+
+  #validateAmounts() {
+    let amount = 0;
+    this.#menus.forEach(value => {
+      amount += value;
+    });
+
+    if (amount > SETTING.maximumMenusAmount) {
+      throw new MenuAmountError();
+    }
+  }
+
+  #validateOnlyBeverage() {
+    if (this.#onlyBeverage()) {
+      throw new MenuOnlyBeverageError();
+    }
+  }
+
+  list() {
+    const string = [];
+
+    this.#menus.forEach((value, key) => {
+      string.push(`${key.get().name} ${value}개`);
+    });
+
+    return string;
+  }
+
+  previousPrice() {
+    let price = 0;
+    this.#menus.forEach((value, key) => {
+      price += key.get().price * value;
+    });
+
+    return price;
+  }
+
+  canApplyEvent() {
+    if (this.previousPrice() > SETTING.minimumApplyEventPrice) {
+      return true;
+    }
+    return false;
+  }
+
+  #onlyBeverage() {
+    let notBeverage = 0;
+    this.#menus.forEach((value, key) => {
+      if (key.get().type !== 'beverage') {
+        notBeverage += value;
+      }
+    });
+
+    if (notBeverage === 0) {
+      return true;
+    }
+    return false;
+  }
+
+  types() {
+    const types = { appetizer: 0, main: 0, beverage: 0, dessert: 0 };
+    this.#menus.forEach((value, key) => {
+      types[key.get().type] += value;
+    });
+
+    return types;
+  }
+}

리뷰: ```js
export default class Menus {
	#menus = new Map();

	constructor(menus) {
```
이렇게 작성 할 수도 있어요!
@@ -0,0 +1,19 @@
+const MENU_LIST = Object.freeze({
+  mushRoomSoup: { name: '양송이수프', type: 'appetizer', price: 6000 },
+  tapas: { name: '타파스', type: 'appetizer', price: 5500 },
+  caesarSalad: { name: '시저샐러드', type: 'appetizer', price: 8000 },
+
+  tBoneSteak: { name: '티본스테이크', type: 'main', price: 55000 },
+  barbecueRibs: { name: '바비큐립', type: 'main', price: 54000 },
+  seafoodPasta: { name: '해산물파스타', type: 'main', price: 35000 },
+  christmasPasta: { name: '크리스마스파스타', type: 'main', price: 25000 },
+
+  chocolateCake: { name: '초코케이크', type: 'dessert', price: 15000 },
+  iceCream: { name: '아이스크림', type: 'dessert', price: 5000 },
+
+  cokeZero: { name: '제로콜라', type: 'beverage', price: 3000 },
+  redWine: { name: '레드와인', type: 'beverage', price: 60000 },
+  champagne: { name: '샴페인', type: 'beverage', price: 25000 },
+});
+
+export default MENU_LIST;

리뷰: ```js
const CATEGORIES = Object.freeze({
	appetizer: 'appetizer',
	main: 'main',
	dessert: 'dessert',
	beverage: 'beverage',
});

const MENU_LIST = Object.freeze({
	mushRoomSoup: { name: '양송이수프', type: CATEGORIES.appetizer, price: 6000 },
	tapas: { name: '타파스', type: CATEGORIES.appetizer, price: 5500 },
	caesarSalad: { name: '시저샐러드', type: CATEGORIES.appetizer, price: 8000 },
```

이부분도 상수처리가 가능할 것 같아요!
@@ -0,0 +1,24 @@
+const PREFIX = '[ERROR]';
+
+const ERROR = Object.freeze({
+  dateType: `${PREFIX} 유효하지 않은 날짜입니다. 다시 입력해 주세요.`,
+
+  menuOrderType: `${PREFIX} 유효하지 않은 주문입니다. 다시 입력해 주세요.`,
+  menuDuplicated: `${PREFIX} 유효하지 않은 주문입니다. 다시 입력해 주세요.`,
+  menuNotExist: `${PREFIX} 유효하지 않은 주문입니다. 다시 입력해 주세요.`,
+
+  menuAmount: `${PREFIX} 한번에 주문 가능한 메뉴의 최대 갯수는 20개 입니다. 다시 입력해 주세요.`,
+  menuOnlyBeverage: `${PREFIX} 음료만 주문할 수 없습니다. 다시 입력해 주세요.`,
+
+  menuName: `${PREFIX} 생성된 메뉴의 이름이 String이 아닙니다.`,
+  menuType: `${PREFIX} 생성된 메뉴의 타입이 String이 아닙니다.`,
+  menuPrice: `${PREFIX} 생성된 메뉴의 가격이 Number이 아닙니다.`,
+
+  eventType: `${PREFIX} 생성된 이벤트의 타입이 String이 아닙니다.`,
+  eventStatus: `${PREFIX} 생성된 이벤트의 상태가 Boolean이 아닙니다.`,
+  eventPrice: `${PREFIX} 생성된 이벤트의 가격이 Number이 아닙니다.`,
+
+  eventNotExist: `${PREFIX} 존재하지 않는 이벤트 입니다.`,
+});
+
+export default ERROR;

리뷰: 타입체킹 👍👍
저는 사실 타입 체킹 에러는 동적 언어의 어쩔수 없는 한계라 사용자에게 보여지는 상황 자체가 있어선 안된다고 생각합니다ㅎㅎ...
@@ -0,0 +1,24 @@
+const PREFIX = '[ERROR]';
+
+const ERROR = Object.freeze({
+  dateType: `${PREFIX} 유효하지 않은 날짜입니다. 다시 입력해 주세요.`,
+
+  menuOrderType: `${PREFIX} 유효하지 않은 주문입니다. 다시 입력해 주세요.`,
+  menuDuplicated: `${PREFIX} 유효하지 않은 주문입니다. 다시 입력해 주세요.`,
+  menuNotExist: `${PREFIX} 유효하지 않은 주문입니다. 다시 입력해 주세요.`,
+
+  menuAmount: `${PREFIX} 한번에 주문 가능한 메뉴의 최대 갯수는 20개 입니다. 다시 입력해 주세요.`,
+  menuOnlyBeverage: `${PREFIX} 음료만 주문할 수 없습니다. 다시 입력해 주세요.`,
+
+  menuName: `${PREFIX} 생성된 메뉴의 이름이 String이 아닙니다.`,
+  menuType: `${PREFIX} 생성된 메뉴의 타입이 String이 아닙니다.`,
+  menuPrice: `${PREFIX} 생성된 메뉴의 가격이 Number이 아닙니다.`,
+
+  eventType: `${PREFIX} 생성된 이벤트의 타입이 String이 아닙니다.`,
+  eventStatus: `${PREFIX} 생성된 이벤트의 상태가 Boolean이 아닙니다.`,
+  eventPrice: `${PREFIX} 생성된 이벤트의 가격이 Number이 아닙니다.`,
+
+  eventNotExist: `${PREFIX} 존재하지 않는 이벤트 입니다.`,
+});
+
+export default ERROR;

리뷰: 그렇군요! 좋은 정보 감사합니다 :)
@@ -0,0 +1,19 @@
+const MENU_LIST = Object.freeze({
+  mushRoomSoup: { name: '양송이수프', type: 'appetizer', price: 6000 },
+  tapas: { name: '타파스', type: 'appetizer', price: 5500 },
+  caesarSalad: { name: '시저샐러드', type: 'appetizer', price: 8000 },
+
+  tBoneSteak: { name: '티본스테이크', type: 'main', price: 55000 },
+  barbecueRibs: { name: '바비큐립', type: 'main', price: 54000 },
+  seafoodPasta: { name: '해산물파스타', type: 'main', price: 35000 },
+  christmasPasta: { name: '크리스마스파스타', type: 'main', price: 25000 },
+
+  chocolateCake: { name: '초코케이크', type: 'dessert', price: 15000 },
+  iceCream: { name: '아이스크림', type: 'dessert', price: 5000 },
+
+  cokeZero: { name: '제로콜라', type: 'beverage', price: 3000 },
+  redWine: { name: '레드와인', type: 'beverage', price: 60000 },
+  champagne: { name: '샴페인', type: 'beverage', price: 25000 },
+});
+
+export default MENU_LIST;

리뷰: 오 이렇게도 작성할 수 있군요! 좋은 정보 감사합니다 :)
@@ -0,0 +1,33 @@
+const SETTING = Object.freeze({
+  date: {
+    minimun: 1,
+    maximum: 31,
+    christmas: 25,
+    friday: 1,
+    saturday: 2,
+    sunday: 3,
+  },
+  weekDays: 7,
+
+  locale: 'ko-KR',
+  menuSplit: ',',
+  menuAmountSplit: '-',
+
+  maximumMenusAmount: 20,
+  minimumApplyEventPrice: 10000,
+
+  christmasDiscount: { default: -1000, forDay: -100 },
+  weekDiscount: -2023,
+  specialDiscount: -1000,
+
+  minimumPresentPrice: 120000,
+  presentMenu: '샴페인',
+
+  minimumAmountBadge: {
+    santa: -20000,
+    tree: -10000,
+    star: -5000,
+  },
+});
+
+export default SETTING;

리뷰: 악 그렇네요! 전혀 인지하지 못하고 있었습니다 ㅎㅎ
바로 설치했습니다 감사합니다!!!
@@ -0,0 +1,87 @@
+import { SETTING } from '../constant/index.js';
+import { MenuRepository } from '../repository/index.js';
+import {
+  MenuAmountError,
+  MenuOnlyBeverageError,
+} from '../error/CustomError.js';
+
+export default class Menus {
+  #menus;
+
+  constructor(menus) {
+    this.#menus = new Map();
+
+    Object.keys(menus).forEach(key => {
+      this.#menus.set(MenuRepository.getMenuByName(key), menus[key]);
+    });
+
+    this.#validateAmounts();
+    this.#validateOnlyBeverage();
+  }
+
+  #validateAmounts() {
+    let amount = 0;
+    this.#menus.forEach(value => {
+      amount += value;
+    });
+
+    if (amount > SETTING.maximumMenusAmount) {
+      throw new MenuAmountError();
+    }
+  }
+
+  #validateOnlyBeverage() {
+    if (this.#onlyBeverage()) {
+      throw new MenuOnlyBeverageError();
+    }
+  }
+
+  list() {
+    const string = [];
+
+    this.#menus.forEach((value, key) => {
+      string.push(`${key.get().name} ${value}개`);
+    });
+
+    return string;
+  }
+
+  previousPrice() {
+    let price = 0;
+    this.#menus.forEach((value, key) => {
+      price += key.get().price * value;
+    });
+
+    return price;
+  }
+
+  canApplyEvent() {
+    if (this.previousPrice() > SETTING.minimumApplyEventPrice) {
+      return true;
+    }
+    return false;
+  }
+
+  #onlyBeverage() {
+    let notBeverage = 0;
+    this.#menus.forEach((value, key) => {
+      if (key.get().type !== 'beverage') {
+        notBeverage += value;
+      }
+    });
+
+    if (notBeverage === 0) {
+      return true;
+    }
+    return false;
+  }
+
+  types() {
+    const types = { appetizer: 0, main: 0, beverage: 0, dessert: 0 };
+    this.#menus.forEach((value, key) => {
+      types[key.get().type] += value;
+    });
+
+    return types;
+  }
+}

리뷰: `Events`에게 위임하는 것이 조금 더 통일성이 있을 것 같네요 :)
@@ -0,0 +1,12 @@
+package christmas.message;
+
+public class ExceptionMessage {
+    public static final String ERROR_PREFIX = "[ERROR] ";
+    public static final String INVALID_DATE = "유효하지 않은 날짜입니다. 다시 입력해 주세요.";
+    public static final String INVALID_ORDER = "유효하지 않은 주문입니다. 다시 입력해 주세요.";
+    public static final String INVALID_MONEY = "유효하지 않은 금액입니다. 다시 입력해 주세요.";
+
+    private ExceptionMessage() {
+        // 불필요한 인스턴스화 방지
+    }
+}

리뷰: 상수 관리를 위해서 지금과 같이 상수 클래스를 사용하실 경우 해당 코드와 같이 인스턴스화를 방지하기 위해 생성자를 숨기는 것은 정말 좋은 접근이라고 생각해요!
같은 관점에서, 저는 4주차 과제에선 상수 클래스를 인터페이스로 사용했는데요.

인터페이스는 
1. 객체 생성이 불가능하고
2. 내부적으로 상수와 추상 메서드만 가질 수 있고,
 3. 상수 선언시 `public static final`을 생략할 수 있기에

상수 클래스로 사용하기에 적절하다고 생각합니다! 
인터페이스도 한번 고려해 보세요 :)

+) 인터페이스를 implements하여 상수 값을 가지고 오는 것은 일종의 안티패턴이라고 하니 한번쯤 찾아 보시는 것도 좋을 것 같습니다!!
@@ -0,0 +1,35 @@
+package christmas.domain;
+
+import christmas.message.ExceptionMessage;
+
+public record Money(int amount) {
+    public Money {
+        validateAmount(amount);
+    }
+
+    private void validateAmount(int amount) {
+        if (amount < 0) {
+            throw new IllegalArgumentException(ExceptionMessage.INVALID_MONEY);
+        }
+    }
+
+    public static Money sum(Money money1, Money money2) {
+        return new Money(money1.amount + money2.amount);
+    }
+
+    public boolean isMoreOrEqualThan(Money source) {
+        return amount >= source.amount;
+    }
+
+    public Money minus(Money money) {
+        return new Money(amount - money.amount);
+    }
+
+    public Money multiply(int count) {
+        return new Money(amount * count);
+    }
+
+    public boolean isZero() {
+        return amount == 0;
+    }
+}

리뷰: 돈과 관련된 정보를 래핑해서 검증된 값만 가지도록 구현하신거 너무 좋은데요 👍 
불변 객체로 만든것도 좋습니다! 메뉴 가격에서 `final Money` 변수를 사용한다면 외부에서 메뉴의 가격이 변경될 일도 없어 좋겠어요
@@ -0,0 +1,33 @@
+package christmas.domain;
+
+public enum EventBadge {
+    SANTA("산타", new Money( 20000)),
+    TREE("트리", new Money( 10000)),
+    STAR("별", new Money(5000)),
+    NONE("없음", new Money(0));
+
+    private final String name;
+    private final Money price;
+
+    EventBadge(String name, Money price) {
+        this.name = name;
+        this.price = price;
+    }
+
+    public static EventBadge findByTotalDiscountPrice(Money totalDiscountMoney) {
+        if (totalDiscountMoney.isMoreOrEqualThan(SANTA.price)) {
+            return SANTA;
+        }
+        if (totalDiscountMoney.isMoreOrEqualThan(TREE.price)) {
+            return TREE;
+        }
+        if (totalDiscountMoney.isMoreOrEqualThan(STAR.price)) {
+            return STAR;
+        }
+        return NONE;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

리뷰: 이 부분에선 제가 3주차때 받았던 피드백을 공유해 드릴게요!
커뮤니티에서 _ 닉네임으로 활동하시는 최승호님 피드백입니다.

>enum은 boolean, int 등의 flag를 좀 더 명시적인 상태로 표현하는 용도로 많이 사용되고,
>
>관례적으로 enum constant 중 유효하지 않은 상태, 없음을 나타내는 요소를 맨 위에 두는 것이 권장됩니다.
>
>유효하지 않은 상태는 1개인 경우가 많은데, 상태가 많아질 경우 유효하지 않은 상태를 항상 맨 위에 두면 상태가 아무리 많아도 찾는데 어려움이 없기 때문입니다.

위와 같은 이유로 `NONE`의 위치를 가장 위에 위치시키는 것을 고려해 보시면 좋을 것 같아요
@@ -0,0 +1,14 @@
+package christmas.domain.menu;
+
+public enum FoodType {
+    APPETIZER("애피타이저"),
+    DESSERT("디저트"),
+    MAIN_COURSE("메인"),
+    DRINK("음료");
+
+    private final String name;
+
+    FoodType(String name) {
+        this.name = name;
+    }
+}

리뷰: 해당 `enum`에서 이름 정보를 저장하신 것은 확장성을 고려한 것인가요?
메뉴판을 출력해 달라는 요구사항이 생기는 등의 변화에는 대처하기 좋을 것 같아요.
@@ -0,0 +1,48 @@
+package christmas.domain.menu;
+
+import christmas.domain.Money;
+import java.util.Arrays;
+
+public enum Menu {
+    MUSHROOM_CREAM_SOUP("양송이수프", FoodType.APPETIZER, new Money(6000)),
+    TAPAS("타파스", FoodType.APPETIZER, new Money(5500)),
+    CAESAR_SALAD("시저샐러드", FoodType.APPETIZER, new Money(8000)),
+    T_BORN_STAKE("티본스테이크", FoodType.MAIN_COURSE, new Money(55000)),
+    BBQ_RIBS("바비큐립", FoodType.MAIN_COURSE, new Money(54000)),
+    SEA_FOOD_PASTA("해산물파스타", FoodType.MAIN_COURSE, new Money(35000)),
+    CHRISTMAS_PASTA("크리스마스파스타", FoodType.MAIN_COURSE, new Money(25000)),
+    CHOCO_CAKE("초코케이크", FoodType.DESSERT, new Money(15000)),
+    ICE_CREAM("아이스크림", FoodType.DESSERT, new Money(5000)),
+    ZERO_COKE("제로콜라", FoodType.DRINK, new Money(3000)),
+    RED_WINE("레드와인", FoodType.DRINK, new Money(60000)),
+    CHAMPAGNE("샴페인", FoodType.DRINK, new Money(25000))
+    ;
+    private final String name;
+    private final FoodType foodType;
+    private final Money price;
+
+    Menu(String name, FoodType foodType, Money price) {
+        this.name = name;
+        this.foodType = foodType;
+        this.price = price;
+    }
+
+    public static Menu findByName(String name) {
+        return Arrays.stream(values())
+                .filter(menu -> menu.name.equals(name))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public boolean isSameType(FoodType foodType) {
+        return this.foodType == foodType;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Money getPrice() {
+        return price;
+    }
+}

리뷰: 정-말 사소한 부분이지만, 이와 같이 같은 `enum`에서 다시 세부 카테고리로 구분이 되는 경우에는 개행을 넣어 구분을 해 주는 것도 가독성을 위한 좋은 습관이라 생각해요!
```suggestion
    CAESAR_SALAD("시저샐러드", FoodType.APPETIZER, new Money(8000)),
    
    T_BORN_STAKE("티본스테이크", FoodType.MAIN_COURSE, new Money(55000)),
```
@@ -0,0 +1,48 @@
+package christmas.domain.menu;
+
+import christmas.domain.Money;
+import java.util.Arrays;
+
+public enum Menu {
+    MUSHROOM_CREAM_SOUP("양송이수프", FoodType.APPETIZER, new Money(6000)),
+    TAPAS("타파스", FoodType.APPETIZER, new Money(5500)),
+    CAESAR_SALAD("시저샐러드", FoodType.APPETIZER, new Money(8000)),
+    T_BORN_STAKE("티본스테이크", FoodType.MAIN_COURSE, new Money(55000)),
+    BBQ_RIBS("바비큐립", FoodType.MAIN_COURSE, new Money(54000)),
+    SEA_FOOD_PASTA("해산물파스타", FoodType.MAIN_COURSE, new Money(35000)),
+    CHRISTMAS_PASTA("크리스마스파스타", FoodType.MAIN_COURSE, new Money(25000)),
+    CHOCO_CAKE("초코케이크", FoodType.DESSERT, new Money(15000)),
+    ICE_CREAM("아이스크림", FoodType.DESSERT, new Money(5000)),
+    ZERO_COKE("제로콜라", FoodType.DRINK, new Money(3000)),
+    RED_WINE("레드와인", FoodType.DRINK, new Money(60000)),
+    CHAMPAGNE("샴페인", FoodType.DRINK, new Money(25000))
+    ;
+    private final String name;
+    private final FoodType foodType;
+    private final Money price;
+
+    Menu(String name, FoodType foodType, Money price) {
+        this.name = name;
+        this.foodType = foodType;
+        this.price = price;
+    }
+
+    public static Menu findByName(String name) {
+        return Arrays.stream(values())
+                .filter(menu -> menu.name.equals(name))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public boolean isSameType(FoodType foodType) {
+        return this.foodType == foodType;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Money getPrice() {
+        return price;
+    }
+}

리뷰: 메서드에서 명시적으로 `null`을 반환해 주는 것은 해당 메서드를 사용하는 외부에서 `null`의 존재를 알지 못해 의도치 못한 NPE를 만날 가능성이 생긴다고 생각합니다!
`null` 대신 유효하지 않은 주문을 명시하는 `enum` 상수를 하나 사용하거나, `Optional`을 사용해 유효하지 않은 상태에 대한 조금 더 안전한 정보를 반환하는 것은 어떨까요?
그렇지 않다면, 해당 메서드에서 바로 예외를 발생 시키는 것 역시 하나의 방법이 될 수 있을 것 같아요!
@@ -0,0 +1,55 @@
+package christmas.domain.menu;
+
+import christmas.message.ExceptionMessage;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
+
+public class OrderParser {
+    private static final int MAX_MENU_COUNT = 20;
+    private static final String DELIMITER_MENU_ORDER = ",";
+
+    private OrderParser() {
+        // 불필요한 인스턴스 생성 방지
+    }
+
+    public static Map<Menu, Integer> parse(String inputValue) {
+        Map<Menu, Integer> orderRepository = new EnumMap<>(Menu.class);
+        Arrays.stream(inputValue.split(DELIMITER_MENU_ORDER))
+                        .forEach(menuString -> addMenuToOrderRepository(orderRepository, menuString));
+        validateOnlyDrink(orderRepository);
+        validateMaxMenuCount(orderRepository);
+        return orderRepository;
+    }
+
+    private static void addMenuToOrderRepository(Map<Menu, Integer> orderRepository, String menuString) {
+        Order order = Order.createByString(menuString);
+        validateDuplicateMenu(orderRepository, order.menu());
+        orderRepository.put(order.menu(), order.count());
+    }
+
+    private static void validateDuplicateMenu(Map<Menu, Integer> result, Menu menu) {
+        if (result.containsKey(menu)) {
+            throw new IllegalArgumentException(ExceptionMessage.INVALID_ORDER);
+        }
+    }
+
+    private static void validateOnlyDrink(Map<Menu, Integer> result) {
+        int drinkMenuCount = (int) result.keySet()
+                .stream()
+                .filter(menu -> menu.isSameType(FoodType.DRINK))
+                .count();
+        if (drinkMenuCount == result.size()) {
+            throw new IllegalArgumentException(ExceptionMessage.INVALID_ORDER);
+        }
+    }
+
+    private static void validateMaxMenuCount(Map<Menu, Integer> result) {
+        Integer totalMenuCount = result.values()
+                .stream()
+                .reduce(0, Integer::sum);
+        if (totalMenuCount > MAX_MENU_COUNT) {
+            throw new IllegalArgumentException(ExceptionMessage.INVALID_ORDER);
+        }
+    }
+}

리뷰: `Parser`라는 이름과 `parse`라는 메서드 명에 비해 너무 많은 역할이 내부에 구현되어 있는 것 같아요.
각각의 주문 목록들을 `EnumMap`에 저장하는 역할은 이곳에서 분리해서 별도의 컬렉션을 래핑하는 클래스를 만들 수 있을 것 같습니다!
@@ -0,0 +1,55 @@
+package christmas.domain.menu;
+
+import christmas.message.ExceptionMessage;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
+
+public class OrderParser {
+    private static final int MAX_MENU_COUNT = 20;
+    private static final String DELIMITER_MENU_ORDER = ",";
+
+    private OrderParser() {
+        // 불필요한 인스턴스 생성 방지
+    }
+
+    public static Map<Menu, Integer> parse(String inputValue) {
+        Map<Menu, Integer> orderRepository = new EnumMap<>(Menu.class);
+        Arrays.stream(inputValue.split(DELIMITER_MENU_ORDER))
+                        .forEach(menuString -> addMenuToOrderRepository(orderRepository, menuString));
+        validateOnlyDrink(orderRepository);
+        validateMaxMenuCount(orderRepository);
+        return orderRepository;
+    }
+
+    private static void addMenuToOrderRepository(Map<Menu, Integer> orderRepository, String menuString) {
+        Order order = Order.createByString(menuString);
+        validateDuplicateMenu(orderRepository, order.menu());
+        orderRepository.put(order.menu(), order.count());
+    }
+
+    private static void validateDuplicateMenu(Map<Menu, Integer> result, Menu menu) {
+        if (result.containsKey(menu)) {
+            throw new IllegalArgumentException(ExceptionMessage.INVALID_ORDER);
+        }
+    }
+
+    private static void validateOnlyDrink(Map<Menu, Integer> result) {
+        int drinkMenuCount = (int) result.keySet()
+                .stream()
+                .filter(menu -> menu.isSameType(FoodType.DRINK))
+                .count();
+        if (drinkMenuCount == result.size()) {
+            throw new IllegalArgumentException(ExceptionMessage.INVALID_ORDER);
+        }
+    }
+
+    private static void validateMaxMenuCount(Map<Menu, Integer> result) {
+        Integer totalMenuCount = result.values()
+                .stream()
+                .reduce(0, Integer::sum);
+        if (totalMenuCount > MAX_MENU_COUNT) {
+            throw new IllegalArgumentException(ExceptionMessage.INVALID_ORDER);
+        }
+    }
+}

리뷰: 모두 음료수인지 확인하는 스트림 체인 로직을 리드미 공장장 해빈님 코드에서 훔쳐왔어요....!
타칸님 코드에 맞게 수정해 보았습니다 ^^
```suggestion
        boolean isAllDrink = result.keySet()
                .stream()
                .allMatch(menu -> menu.isSameType(FoodType.DRINK));
        if(isAllDrink){
            throw new IllegalArgumentException(ExceptionMessage.INVALID_ORDER);
        }
```