ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,26 @@
+package christmas.domain;
+
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ChristmasDDayDiscountCreator implements TotalDiscountCreator {
+
+    private static final Map<Integer, TotalDiscount> CACHE;
+
+    static {
+        CACHE = new HashMap<>();
+
+        TotalDiscountType.CHRISTMAS_D_DAY.getDays()
+            .forEach(day -> CACHE.put(day,
+                new TotalDiscount(new Money(1000 + (day - 1) * 100),
+                    TotalDiscountType.CHRISTMAS_D_DAY)));
+    }
+
+    @Override
+    public TotalDiscount from(LocalDate date) {
+        int dayOfMonth = date.getDayOfMonth();
+
+        return CACHE.getOrDefault(dayOfMonth, TotalDiscount.NONE);
+    }
+}

리뷰: 이 부분도 상수화 한다면 숫자의 의미도 명확히 알 수 있고 좋을 것 같습니다! :)
@@ -0,0 +1,28 @@
+package christmas.domain;
+
+import java.util.List;
+
+public class MenuDiscounts {
+
+    private static final MenuDiscounts NONE = new MenuDiscounts(List.of());
+
+    private List<MenuDiscount> menuDiscounts;
+
+    public MenuDiscounts(List<MenuDiscount> menuDiscounts) {
+        this.menuDiscounts = menuDiscounts;
+    }
+
+    public Money sumOfWeekDaysDiscounts() {
+        return menuDiscounts.stream()
+            .filter(MenuDiscount::isWeekdays)
+            .map(MenuDiscount::getMoney)
+            .reduce(Money.ZERO, Money::plus);
+    }
+
+    public Money sumOfWeekendsDiscounts() {
+        return menuDiscounts.stream()
+            .filter(MenuDiscount::isWeekends)
+            .map(MenuDiscount::getMoney)
+            .reduce(Money.ZERO, Money::plus);
+    }
+}

리뷰: 스트림을 되게 잘 사용하시는 것 같아요..!
혹시 공부하시는 팁(??) 이랄 게 있으실지.. 여쭤봅니다 ㅎㅎ
@@ -0,0 +1,46 @@
+package christmas.domain;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum MenuGroup {
+    APPETIZER("애피타이저", Arrays.asList(
+        MenuDetail.MUSHROOM_SOUP,
+        MenuDetail.TAPAS,
+        MenuDetail.CAESAR_SALAD)),
+    MAIN("메인", Arrays.asList(
+        MenuDetail.T_BONE_STEAK,
+        MenuDetail.BARBECUE_RIBS,
+        MenuDetail.SEAFOOD_PASTA,
+        MenuDetail.CHRISTMAS_PASTA)),
+    DESSERT("디저트", Arrays.asList(
+        MenuDetail.CHOCOLATE_CAKE,
+        MenuDetail.ICE_CREAM
+    )),
+    DRINK("음료", Arrays.asList(
+        MenuDetail.ZERO_COLA,
+        MenuDetail.RED_WINE,
+        MenuDetail.CHAMPAGNE
+    ));
+
+    private static final String MENU_GROUP_NOT_FOUND_ERROR_MESSAGE = "[ERROR] 메뉴를 확인해주세요.";
+
+    private String title;
+    private List<MenuDetail> menuDetails;
+
+    MenuGroup(String title, List<MenuDetail> menuDetails) {
+        this.title = title;
+        this.menuDetails = menuDetails;
+    }
+
+    public static MenuGroup findByMenuDetail(MenuDetail menuDetail) {
+        return Arrays.stream(MenuGroup.values())
+            .filter(menuGroup -> menuGroup.hasMenuDetail(menuDetail))
+            .findFirst()
+            .orElseThrow(() -> new IllegalArgumentException(MENU_GROUP_NOT_FOUND_ERROR_MESSAGE));
+    }
+
+    public boolean hasMenuDetail(MenuDetail menuDetail) {
+        return menuDetails.contains(menuDetail);
+    }
+}
\ No newline at end of file

리뷰: 해당 에러 메시지가 조금 모호한 것 같아요
해당 메뉴는 우테코 식당에서 판매하지 않습니다 등과 같은 조금 특정한 뜻을 담는 것은 어떨까요?
@@ -0,0 +1,26 @@
+package christmas.dtos;
+
+import christmas.domain.BenefitType;
+
+public class BenefitDto {
+
+    private BenefitType benefitType;
+    private int discountAmount;
+
+    BenefitDto(BenefitType benefitType, int discountAmount) {
+        this.benefitType = benefitType;
+        this.discountAmount = discountAmount;
+    }
+
+    public static BenefitDto toDto(BenefitType benefitType, int discountAmount) {
+        return new BenefitDto(benefitType, discountAmount);
+    }
+
+    public BenefitType getBenefitType() {
+        return benefitType;
+    }
+
+    public int getDiscountAmount() {
+        return discountAmount;
+    }
+}

리뷰: dto는 record 클래스를 사용하면 쉽게 구현 가능하더라구요, 참고하시면 좋을 것 같습니다!
@@ -0,0 +1,80 @@
+package christmas.controller;
+
+import christmas.domain.BenefitCalculationFactory;
+import christmas.domain.Benefits;
+import christmas.domain.EventPlanner;
+import christmas.domain.Order;
+import christmas.dtos.OrderLineDto;
+import christmas.util.OrderLineMapper;
+import christmas.view.ErrorMessage;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+import java.time.LocalDate;
+import java.util.List;
+
+public class ChristmasController {
+
+    public static void run() {
+        OutputView.printWelcomeMessage();
+        int day = getExpectedVisitDate();
+
+        Order order = getOrder();
+
+        LocalDate orderDate = LocalDate.of(2023, 12, day);
+
+        Benefits benefits = BenefitCalculationFactory.calculateBenefits(order, orderDate);
+
+        OutputView.printBenefitPreviewMessage(orderDate);
+
+        EventPlanner.printPreview(order, benefits);
+    }
+
+    public static Order getOrder() {
+        try {
+            OutputView.printOrderMenuMessage();
+            List<OrderLineDto> dtos = InputView.readMenus();
+            validateDuplicateMenus(dtos);
+
+            Order order = new Order(OrderLineMapper.orderLineDtosToOrderLines(dtos));
+
+            return order;
+        } catch (IllegalArgumentException e) {
+            OutputView.printExceptionMessage(e);
+            return getOrder();
+        }
+    }
+
+
+    public static int getExpectedVisitDate() {
+        try {
+            OutputView.printExpectedVisitDateMessage();
+            int day = InputView.readInteger();
+            validateExpectedVisitDate(day);
+            return day;
+        } catch (IllegalArgumentException e) {
+
+            System.out.println(e.getMessage());
+            System.out.println(e.getClass());
+            OutputView.printExceptionMessage(e);
+            return getExpectedVisitDate();
+        }
+    }
+
+    private static void validateExpectedVisitDate(int day) {
+        if (day < 1 || day > 31) {
+            throw new IllegalArgumentException(ErrorMessage.INVALID_DATE_INPUT.getMessage());
+        }
+    }
+
+    private static void validateDuplicateMenus(List<OrderLineDto> dtos) {
+        int originalSize = dtos.size();
+        int distinctSize = (int) dtos.stream()
+            .map(OrderLineDto::getMenuDetail)
+            .distinct()
+            .count();
+
+        if (originalSize != distinctSize) {
+            throw new IllegalArgumentException(ErrorMessage.DUPLICATE_MENU_INPUT.getMessage());
+        }
+    }
+}

리뷰: 맞습니다!.. 과제에서 의미 있는 숫자들을 상수로 리팩토링 하는 부분이 전반적으로 부족했다 생각합니다.🥲
@@ -0,0 +1,26 @@
+package christmas.domain;
+
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ChristmasDDayDiscountCreator implements TotalDiscountCreator {
+
+    private static final Map<Integer, TotalDiscount> CACHE;
+
+    static {
+        CACHE = new HashMap<>();
+
+        TotalDiscountType.CHRISTMAS_D_DAY.getDays()
+            .forEach(day -> CACHE.put(day,
+                new TotalDiscount(new Money(1000 + (day - 1) * 100),
+                    TotalDiscountType.CHRISTMAS_D_DAY)));
+    }
+
+    @Override
+    public TotalDiscount from(LocalDate date) {
+        int dayOfMonth = date.getDayOfMonth();
+
+        return CACHE.getOrDefault(dayOfMonth, TotalDiscount.NONE);
+    }
+}

리뷰: 감사합니다 :> 맞는 말씀이라 생각합니다
@@ -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;

리뷰: 오류 메시지가 여러 개인 건 좋지만 실질적인 사용자 입장에서는 String, Number, Boolean이 어떤건지 모르실 것 같아요!
그리고 같은 문구인데 여러 개로 만드신 이유가 있을까요?
@@ -0,0 +1,70 @@
+import Menus from './Menus.js';
+import Order from './Order.js';
+import { REGEXP, SETTING } from '../constant/index.js';
+import { Date } from '../model/index.js';
+import { OutputView, InputView } from '../view/index.js';
+import {
+  MenuDuplicatedError,
+  MenuOrderTypeError,
+} from '../error/CustomError.js';
+
+export default class GetOrder {
+  #order;
+
+  async startOrder() {
+    const date = await this.#readDate();
+    const menus = await this.#readMenus();
+    this.#order = new Order(date, menus);
+  }
+
+  async #readDate() {
+    while (true) {
+      const date = await InputView.readDate();
+      try {
+        return new Date(date);
+      } catch (error) {
+        OutputView.printErrorMessage(error);
+      }
+    }
+  }
+
+  async #readMenus() {
+    while (true) {
+      const menus = await InputView.readMenus();
+      try {
+        this.#validateInputType(menus);
+        return this.#stringToMenus(menus);
+      } catch (error) {
+        OutputView.printErrorMessage(error);
+      }
+    }
+  }
+
+  #validateInputType(string) {
+    if (!REGEXP.menus.test(string)) {
+      throw new MenuOrderTypeError();
+    }
+  }
+
+  #validateMenuDuplicated(inputLength, resultLength) {
+    if (inputLength !== resultLength) {
+      throw new MenuDuplicatedError();
+    }
+  }
+
+  #stringToMenus(string) {
+    const menus = {};
+    string.split(SETTING.menuSplit).forEach(menuInfo => {
+      const menu = menuInfo.split(SETTING.menuAmountSplit)[0];
+      const amount = menuInfo.split(SETTING.menuAmountSplit)[1];
+      menus[`${menu}`] = Number(amount);
+    });
+
+    this.#validateMenuDuplicated(
+      string.split(SETTING.menuSplit).length,
+      Object.keys(menus).length,
+    );
+
+    return new Menus(menus);
+  }
+}

리뷰: 0과 1이 상수라면 해당 부분도 변수화해서 의미를 주는 것도 괜찮아보입니다 !
@@ -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;
+  }
+}

리뷰: 오 이렇게 하는 방법이 있었네요
Lotto 때 사용해놓고 이번에 이렇게 안했네요...!
@@ -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,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;
+  }
+}

리뷰: 저는 이렇게 사용하고, 음료주문 validate에서 이걸 사용하려고 했던건데, 이상하게
음료주문 validate에서 이 types를 불러오면 테스트코드가 무한루프에 빠지더라구요...
오류를 찾는데 3-4시간정도 시간을 쓰다가 결국 새로운 메소드를 만들었습니다.. 하하
@@ -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,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;
+  }
+}

리뷰: 메뉴 유효성 검사에 대한 코드 분리를 따로 하지 않고, 메뉴 클래스내에서 진행하시는 방법을 택하신거 같네요! 저는 4주동안 매번 유효성 검사 함수, 클래스를 따로 분리하여 호출하였는데요 ! 혹시, 어떤 의도나 생각으로 구성하고자 하셨는지 의견이 궁금합니다 ㅎㅎ
@@ -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;
+  }
+}

리뷰: 저는 음료만 주문한 에러 처리에 대해서 타입의 개수가 1개만 있는데(has 사용), 그게 drink 인 경우라고 작성을 했었어요
이게 좀더 명료해서 좋은 거 같네용 배우고 가요 ☺️
@@ -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,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;
+  }
+}

리뷰: 사실 저도 2주차까지는 유효성검사를 따로 분리해서 진행했었습니다!
하지만, 3주차 과제 Lotto에서, 주어진 Lotto 클래스 내부에 유효성검사를 진행하는 코드를 주어졌길래
이렇게도 할 수 있나? 우테코가 원하는 방식은 무엇일까? 생각하며 비슷하게 하려고 했던 것 같네요 ㅎㅎ
아직 코린이라 엄청난 의도나 생각이 있었던 것은 아니었습니다 :)
@@ -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;
+  }
+}

리뷰: 여경님의 방법도 나쁘지 않은 것 같습니다! 코드를 읽는 입장에서 그 편이 훨씬 깔끔할 것 같아요 ㅎㅎ
리소스도 미소하게나마 적게 차지할 것 같습니다 :)