ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,46 @@
+package christmas.domain;
+
+import christmas.exception.ErrorMessage;
+import christmas.exception.PromotionException;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.util.List;
+
+public class Date {
+    public static final int EVENT_YEAR = 2023;
+    public static final int EVENT_MONTH = 12;
+    public static final int MIN_DATE = 1;
+    public static final int MAX_DATE = 31;
+    private final int date;
+
+    public Date(int date) {
+        validate(date);
+        this.date = date;
+    }
+
+    public int getDate() {
+        return date;
+    }
+
+    private void validate(int date) {
+        if (date < MIN_DATE || date > MAX_DATE) {
+            throw new PromotionException(ErrorMessage.INVALID_DATE_MESSAGE);
+        }
+    }
+
+    public int dayFromDate(int other) {
+        return date - other;
+    }
+
+    public DayOfWeek dayOfWeek() {
+        return LocalDate.of(EVENT_YEAR, EVENT_MONTH, date).getDayOfWeek();
+    }
+
+    public boolean isIncluded(List<Integer> dates) {
+        return dates.contains(date);
+    }
+
+    public boolean isInRange(int startInclusive, int endInclusive) {
+        return startInclusive <= date && date <= endInclusive;
+    }
+}

리뷰: 날짜를 int 타입으로 하셔서 1일과 31일 범위에 대한 검증 로직을 작성하셨네요. 아무래도 명백히 날짜를 관리하는 클래스다보니 LocalDate를 사용하여 날짜를 관리하면 생성시 잘못된 날짜인 경우에 DateTimeException이 발생하고 이 에러를 잡아서 처리하여 검증하는 방법도 적용해보시면 좀 더 범용적인 날짜를 관리할 수 있을 것 같습니다.
@@ -0,0 +1,32 @@
+package christmas.domain.event;
+
+import christmas.domain.Date;
+import christmas.domain.Menus;
+
+public enum Event {
+    D_DAY_DISCOUNT(new DDayDiscount(), "크리스마스 디데이 할인"),
+    WEEKDAY_DISCOUNT(new WeekdayDiscount(), "평일 할인"),
+    WEEKEND_DISCOUNT(new WeekendDiscount(), "주말 할인"),
+    SPECIAL_DISCOUNT(new SpecialDiscount(), "특별 할인"),
+    GIVEAWAY(new Giveaway(), "증정 이벤트");
+
+    private final EventPolicy policy;
+    private final String description;
+
+    Event(EventPolicy policy, String description) {
+        this.policy = policy;
+        this.description = description;
+    }
+
+    public String description() {
+        return description;
+    }
+
+    public EventPolicy policy() {
+        return policy;
+    }
+
+    public int benefitAmount(Date date, Menus menus) {
+        return policy.amount(date, menus);
+    }
+}

리뷰: 이벤트를 enum에 생성해서 관리하셨네요 저는 이런 생각을 못했는데 새로 배우게 되네요 감사합니다.
@@ -0,0 +1,59 @@
+package christmas.domain.menu;
+
+import static christmas.domain.menu.Category.APPETIZER;
+import static christmas.domain.menu.Category.BEVERAGE;
+import static christmas.domain.menu.Category.DESSERT;
+import static christmas.domain.menu.Category.MAIN;
+
+import java.util.Arrays;
+
+public enum Menu {
+    MUSHROOM_SOUP(APPETIZER, "양송이수프", 6_000),
+    TAPAS(APPETIZER, "타파스", 5_500),
+    CAESAR_SALAD(APPETIZER, "시저샐러드", 8_000),
+
+    T_BONE_STEAK(MAIN, "티본스테이크", 55_000),
+    BARBECUE_RIBS(MAIN, "바비큐립", 54_000),
+    SEAFOOD_PASTA(MAIN, "해산물파스타", 35_000),
+    CHRISTMAS_PASTA(MAIN, "크리스마스파스타", 25_000),
+
+    CHOCOLATE_CAKE(DESSERT, "초코케이크", 15_000),
+    ICE_CREAM(DESSERT, "아이스크림", 5_000),
+
+    ZERO_COLA(BEVERAGE, "제로콜라", 3_000),
+    RED_WINE(BEVERAGE, "레드와인", 60_000),
+    CHAMPAGNE(BEVERAGE, "샴페인", 25_000);
+
+    private final Category category;
+    private final String description;
+    private final int price;
+
+    Menu(Category category, String description, int price) {
+        this.category = category;
+        this.description = description;
+        this.price = price;
+    }
+
+    public static boolean exists(String description) {
+        return Arrays.stream(Menu.values())
+                .anyMatch(m -> description.equals(m.description()));
+    }
+
+    public static Menu fromDescription(String description) {
+        return Arrays.stream(Menu.values())
+                .filter(m -> description.equals(m.description))
+                .findAny().get();
+    }
+
+    public Category category() {
+        return category;
+    }
+
+    public String description() {
+        return description;
+    }
+
+    public int price() {
+        return price;
+    }
+}

리뷰: 각 메뉴의 카테고리도 enum으로 만들어서 관리하는 방법은 어떨까 하네요.
@@ -0,0 +1,21 @@
+package christmas.controller;
+
+import christmas.dto.MenuCount;
+import java.util.Arrays;
+import java.util.List;
+
+public class Parser {
+
+    public static int parseDate(String input) {
+        return Integer.parseInt(input);
+    }
+
+    public static List<String> parseMenus(String input, String delimiter) {
+        return Arrays.stream(input.split(delimiter)).toList();
+    }
+
+    public static MenuCount parseMenu(String input, String delimiter) {
+        String[] split = input.split(delimiter);
+        return new MenuCount(split[0], Integer.parseInt(split[1]));
+    }
+}

리뷰: InputView의 반환 타입을 모두 String으로 적절한 형변환은 컨트롤러에서 하자 라고 생각해서 Parser를 분리했습니다. (지금 보니 date는 InputView에서 파싱을 해놔서 아쉽네요)
@@ -0,0 +1,46 @@
+package christmas.domain;
+
+import christmas.exception.ErrorMessage;
+import christmas.exception.PromotionException;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.util.List;
+
+public class Date {
+    public static final int EVENT_YEAR = 2023;
+    public static final int EVENT_MONTH = 12;
+    public static final int MIN_DATE = 1;
+    public static final int MAX_DATE = 31;
+    private final int date;
+
+    public Date(int date) {
+        validate(date);
+        this.date = date;
+    }
+
+    public int getDate() {
+        return date;
+    }
+
+    private void validate(int date) {
+        if (date < MIN_DATE || date > MAX_DATE) {
+            throw new PromotionException(ErrorMessage.INVALID_DATE_MESSAGE);
+        }
+    }
+
+    public int dayFromDate(int other) {
+        return date - other;
+    }
+
+    public DayOfWeek dayOfWeek() {
+        return LocalDate.of(EVENT_YEAR, EVENT_MONTH, date).getDayOfWeek();
+    }
+
+    public boolean isIncluded(List<Integer> dates) {
+        return dates.contains(date);
+    }
+
+    public boolean isInRange(int startInclusive, int endInclusive) {
+        return startInclusive <= date && date <= endInclusive;
+    }
+}

리뷰: 넵 만약 다른 달도 있었다면 LocalDate 자체를 사용해도 좋았을 것 같습니다! 하지만 이번 미션에 경우 이벤트 달인 2023년 12월에 한정됐기 때문에 int만 감싸서 사용했습니다.
@@ -0,0 +1,74 @@
+package christmas.domain;
+
+import christmas.domain.event.Event;
+import christmas.domain.event.Giveaway;
+import christmas.domain.menu.Category;
+import christmas.domain.menu.Menu;
+import christmas.dto.MenuCount;
+import christmas.exception.ErrorMessage;
+import christmas.exception.PromotionException;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class Order {
+    private final Menus menus;
+    private final Date date;
+
+    public Order(Date date, Menus menus) {
+        validateCategory(menus);
+        this.date = date;
+        this.menus = menus;
+    }
+
+    public int getDate() {
+        return date.getDate();
+    }
+
+    public Map<Menu, Integer> getOrderMenu() {
+        return menus.getMenus();
+    }
+
+    private void validateCategory(Menus menus) {
+        if (menus.isAllInCategory(Category.BEVERAGE)) {
+            throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+        }
+    }
+
+    public int totalAmount() {
+        return menus.totalAmount();
+    }
+
+    public Optional<MenuCount> giveGiveaway() {
+        Giveaway giveaway = (Giveaway) Event.GIVEAWAY.policy();
+        return giveaway.giveGiveaway(date, menus);
+    }
+
+    public Map<Event, Integer> benefitDetails() {
+        return Arrays.stream(Event.values())
+                .map(event -> new SimpleEntry<>(event, event.benefitAmount(date, menus)))
+                .filter(entry -> entry.getValue() < 0)
+                .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (x, y) -> y, LinkedHashMap::new));
+    }
+
+    public int totalBenefit() {
+        return Arrays.stream(Event.values())
+                .mapToInt(event -> event.benefitAmount(date, menus))
+                .sum();
+    }
+
+    public int totalDiscount() {
+        return Arrays.stream(Event.values())
+                .filter(event -> event != Event.GIVEAWAY)
+                .mapToInt(event -> event.benefitAmount(date, menus))
+                .sum();
+    }
+
+    public int finalAmount() {
+        return totalAmount() + totalDiscount();
+    }
+}

리뷰: 위에서 말씀해주신 방법대로 일급 컬렉션을 만들었다면 그렇게 처리할 수도 있었겠네요! 의견 감사합니다!
@@ -0,0 +1,33 @@
+package christmas.domain.event;
+
+import christmas.domain.Date;
+import christmas.domain.Menus;
+import christmas.domain.menu.Menu;
+import christmas.dto.MenuCount;
+import java.util.Optional;
+
+public class Giveaway implements EventPolicy {
+    private static final int MIN_TOTAL_AMOUNT = 120_000; //shadowing
+    private static final Menu giveaway = Menu.CHAMPAGNE;
+    private static final int numGiveaway = 1;
+
+    @Override
+    public boolean canBeApplied(Date date, Menus menus) {
+        return menus.totalAmount() >= MIN_TOTAL_AMOUNT;
+    }
+
+    @Override
+    public int amount(Date date, Menus menus) {
+        if (canBeApplied(date, menus)) {
+            return -1 * numGiveaway * giveaway.price();
+        }
+        return NONE;
+    }
+
+    public Optional<MenuCount> giveGiveaway(Date date, Menus menus) {
+        if (canBeApplied(date, menus)) {
+            return Optional.of(new MenuCount(giveaway.description(), numGiveaway));
+        }
+        return Optional.empty();
+    }
+}

리뷰: 제가 인터페이스 구현에 익숙치 않아서 남겨놓았던 주석입니다. 인터페이스와 구현체가 같은 이름의 변수를 사용하면 구현체의 변수값으로 shadowing 된다는 걸 표시했습니다.
@@ -0,0 +1,85 @@
+package christmas.domain;
+
+import christmas.domain.menu.Category;
+import christmas.domain.menu.Menu;
+import christmas.exception.ErrorMessage;
+import christmas.exception.PromotionException;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class Menus {
+    public static final int MIN_MENU_COUNT = 1;
+    public static final int MAX_ORDER_COUNT = 20;
+    private final Map<Menu, Integer> menus = new EnumMap<>(Menu.class);
+
+    public Map<Menu, Integer> getMenus() {
+        return menus;
+    }
+
+    public void add(String menu, int count) {
+        validate(menu, count);
+        menus.put(Menu.fromDescription(menu), count);
+    }
+
+    private void validate(String menu, int count) {
+        validateMenuExists(menu);
+        validateMenuNotDuplicated(menu);
+        validateCount(count);
+        validateTotalCount(count);
+    }
+
+    public boolean isAllInCategory(Category category) {
+        return menus.keySet().stream()
+                .map(Menu::category)
+                .allMatch(c -> c.equals(category));
+    }
+
+    public int countByCategory(Category category) {
+        return menus.keySet().stream()
+                .filter(key -> category.equals(key.category()))
+                .mapToInt(menus::get)
+                .sum();
+    }
+
+    public int totalCount() {
+        return menus.values().stream()
+                .mapToInt(Integer::intValue)
+                .sum();
+    }
+
+    public int totalAmount() {
+        return menus.entrySet().stream()
+                .mapToInt(this::itemPrice)
+                .sum();
+    }
+
+    private int itemPrice(Entry<Menu, Integer> item) {
+        return item.getKey().price() * item.getValue();
+    }
+
+    private void validateMenuExists(String menu) {
+        if (Menu.exists(menu)) {
+            return;
+        }
+        throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+    }
+
+    private void validateCount(int count) {
+        if (count < MIN_MENU_COUNT) {
+            throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+        }
+    }
+
+    private void validateMenuNotDuplicated(String menu) {
+        if (menus.containsKey(Menu.fromDescription(menu))) {
+            throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+        }
+    }
+
+    private void validateTotalCount(int count) {
+        if (totalCount() + count > MAX_ORDER_COUNT) {
+            throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+        }
+    }
+}

리뷰: 검증 로직과, totalAmount()등의 비지니스 로직은 관련성이 낮아보입니다! 이부분도 Parser처럼 validator로 분리 가능해보여요 !
@@ -0,0 +1,74 @@
+package christmas.view;
+
+import christmas.dto.DiscountAmount;
+import christmas.dto.MenuCount;
+import christmas.exception.PromotionException;
+import java.util.List;
+import java.util.Optional;
+
+public class OutputView {
+
+    public void printPlanHeader(int date) {
+        System.out.printf("12월 %d일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!\n", date);
+    }
+
+    public void printOrder(List<MenuCount> order) {
+        System.out.println("\n<주문 메뉴>");
+        order.forEach(this::printMenuCount);
+    }
+
+    public void printTotalAmount(int amount) {
+        System.out.println("\n<할인 전 총주문 금액>");
+        printAmount(amount);
+    }
+
+    public void printGiveaway(Optional<MenuCount> giveaway) {
+        System.out.println("\n<증정 메뉴>");
+        if (giveaway.isPresent()) {
+            printMenuCount(giveaway.get());
+            return;
+        }
+        printNone();
+    }
+
+    public void printBenefitDetails(List<DiscountAmount> discountAmounts) {
+        System.out.println("\n<혜택 내역>");
+        if (discountAmounts.isEmpty()) {
+            printNone();
+            return;
+        }
+        discountAmounts.forEach(da -> System.out.printf("%s: %,d원\n", da.name(), da.amount()));
+    }
+
+    public void printTotalBenefit(int totalBenefit) {
+        System.out.println("\n<총혜택 금액>");
+        printAmount(totalBenefit);
+    }
+
+    public void printFinalAmount(int finalAMount) {
+        System.out.println("\n<할인 후 예상 결제 금액>");
+        printAmount(finalAMount);
+    }
+
+    public void printBadge(String displayName) {
+        System.out.println("\n<12월 이벤트 배지>");
+        System.out.println(displayName);
+    }
+
+    public void printErrorMessage(PromotionException e) {
+        System.out.println(e.getMessage());
+    }
+
+
+    private void printNone() {
+        System.out.println("없음");
+    }
+
+    private void printMenuCount(MenuCount menuCount) {
+        System.out.printf("%s %d개\n", menuCount.menu(), menuCount.count());
+    }
+
+    private void printAmount(int amount) {
+        System.out.printf("%,d원\n", amount);
+    }
+}

리뷰: 각 출력문을 static final 상수로 처리하면 더 좋을 것 같아요!
@@ -0,0 +1,59 @@
+package christmas.domain.menu;
+
+import static christmas.domain.menu.Category.APPETIZER;
+import static christmas.domain.menu.Category.BEVERAGE;
+import static christmas.domain.menu.Category.DESSERT;
+import static christmas.domain.menu.Category.MAIN;
+
+import java.util.Arrays;
+
+public enum Menu {
+    MUSHROOM_SOUP(APPETIZER, "양송이수프", 6_000),
+    TAPAS(APPETIZER, "타파스", 5_500),
+    CAESAR_SALAD(APPETIZER, "시저샐러드", 8_000),
+
+    T_BONE_STEAK(MAIN, "티본스테이크", 55_000),
+    BARBECUE_RIBS(MAIN, "바비큐립", 54_000),
+    SEAFOOD_PASTA(MAIN, "해산물파스타", 35_000),
+    CHRISTMAS_PASTA(MAIN, "크리스마스파스타", 25_000),
+
+    CHOCOLATE_CAKE(DESSERT, "초코케이크", 15_000),
+    ICE_CREAM(DESSERT, "아이스크림", 5_000),
+
+    ZERO_COLA(BEVERAGE, "제로콜라", 3_000),
+    RED_WINE(BEVERAGE, "레드와인", 60_000),
+    CHAMPAGNE(BEVERAGE, "샴페인", 25_000);
+
+    private final Category category;
+    private final String description;
+    private final int price;
+
+    Menu(Category category, String description, int price) {
+        this.category = category;
+        this.description = description;
+        this.price = price;
+    }
+
+    public static boolean exists(String description) {
+        return Arrays.stream(Menu.values())
+                .anyMatch(m -> description.equals(m.description()));
+    }
+
+    public static Menu fromDescription(String description) {
+        return Arrays.stream(Menu.values())
+                .filter(m -> description.equals(m.description))
+                .findAny().get();
+    }
+
+    public Category category() {
+        return category;
+    }
+
+    public String description() {
+        return description;
+    }
+
+    public int price() {
+        return price;
+    }
+}

리뷰: 리뷰어님의 코드를 보니 말씀하신 방법이 효율적이네요! 의견 감사합니다.
@@ -0,0 +1,42 @@
+package christmas.view;
+
+import static christmas.controller.Parser.parseDate;
+import static christmas.controller.Parser.parseMenus;
+
+import camp.nextstep.edu.missionutils.Console;
+import christmas.exception.ErrorMessage;
+import christmas.exception.PromotionException;
+import java.util.List;
+
+public class InputView {
+    private static final String DATE_PATTERN = "[\\d]{1,2}";
+    private static final String MENUS_PATTERN = "(([가-힣]+)-([\\d]{1,2}),)*([가-힣]+)-([\\d]{1,2})";
+
+    public int readVisitDate() {
+        System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
+        String date = Console.readLine();
+        validateDateInput(date);
+        return parseDate(date);
+    }
+
+    private void validateDateInput(String input) {
+        if (input.matches(DATE_PATTERN)) {
+            return;
+        }
+        throw new PromotionException(ErrorMessage.INVALID_DATE_MESSAGE);
+    }
+
+    public List<String> readMenus() {
+        System.out.println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)");
+        String menus = Console.readLine().trim();
+        validateMenuInput(menus);
+        return parseMenus(menus, ",");
+    }
+
+    private void validateMenuInput(String menus) {
+        if (menus.matches(MENUS_PATTERN)) {
+            return;
+        }
+        throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+    }
+}

리뷰: 정규식에 한글도 되는군요! 배워가요!
@@ -0,0 +1,18 @@
+package christmas.domain.event;
+
+import christmas.domain.Date;
+import christmas.domain.Menus;
+
+public interface EventPolicy {
+    int MIN_TOTAL_AMOUNT = 10_000;
+    int NONE = 0;
+
+
+    default boolean isApplicableMenus(Menus menus) {
+        return menus.totalAmount() >= MIN_TOTAL_AMOUNT;
+    }
+
+    boolean canBeApplied(Date date, Menus menus);
+
+    int amount(Date date, Menus menus);
+}

리뷰: 다른 구현체들 코드 보니까, amount 메서드 내부에서 canBeApplied 메서드가 호출되게 되는데 이를 default 메서드나, 구체클래스와 인터페이스 사이에 추상 클래스를 추가하여 강제한다면 더 좋을 것 같습니다! 현재는 실수가 발생해도 막을 방법이 없기 때문이죠!
@@ -0,0 +1,53 @@
+package christmas.domain.event;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import christmas.domain.Date;
+import christmas.domain.Menus;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class DDayDiscountTest {
+    EventPolicy dDayDiscount = new DDayDiscount();
+
+    @DisplayName("10000원 미만인 경우 할인 적용되지 않는다.")
+    @Test
+    void notApplicableOrder() {
+        Menus menus = new Menus();
+        menus.add(("타파스"), 1);
+
+        assertThat(dDayDiscount.canBeApplied(new Date(25), menus))
+                .isEqualTo(false);
+        assertThat(dDayDiscount.amount(new Date(25), menus))
+                .isEqualTo(0);
+    }
+
+    @DisplayName("할인 기간이 아니면 적용되지 않는다.")
+    @ParameterizedTest
+    @CsvSource({"25,true", "26,false"})
+    void notApplicableDate(int dateSource, boolean expected) {
+        assertThat(dDayDiscount.canBeApplied(new Date(dateSource), createMenus()))
+                .isEqualTo(expected);
+    }
+
+    @DisplayName("할인 금액 테스트")
+    @ParameterizedTest
+    @CsvSource({"1,-1000", "25,-3400"})
+    void amount(int dateSource, int expectedAmount) {
+        assertThat(dDayDiscount.amount(new Date(dateSource), createMenus()))
+                .isEqualTo(expectedAmount);
+    }
+
+    Menus createMenus() {
+        Menus menus = new Menus();
+        menus.add(("타파스"), 2);
+        menus.add(("시저샐러드"), 1);
+        menus.add(("해산물파스타"), 2);
+        menus.add(("초코케이크"), 1);
+        menus.add(("제로콜라"), 1);
+        menus.add(("샴페인"), 1);
+        return menus;
+    }
+}

리뷰: 모든 테스트 코드에서 이 메서드가 존재하는 같습니다. 이를 추상화해서 테스트코드를 작성하면, 더 좋을 것 같습니다!
@@ -0,0 +1,42 @@
+package christmas.view;
+
+import static christmas.controller.Parser.parseDate;
+import static christmas.controller.Parser.parseMenus;
+
+import camp.nextstep.edu.missionutils.Console;
+import christmas.exception.ErrorMessage;
+import christmas.exception.PromotionException;
+import java.util.List;
+
+public class InputView {
+    private static final String DATE_PATTERN = "[\\d]{1,2}";
+    private static final String MENUS_PATTERN = "(([가-힣]+)-([\\d]{1,2}),)*([가-힣]+)-([\\d]{1,2})";
+
+    public int readVisitDate() {
+        System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
+        String date = Console.readLine();
+        validateDateInput(date);
+        return parseDate(date);
+    }
+
+    private void validateDateInput(String input) {
+        if (input.matches(DATE_PATTERN)) {
+            return;
+        }
+        throw new PromotionException(ErrorMessage.INVALID_DATE_MESSAGE);
+    }
+
+    public List<String> readMenus() {
+        System.out.println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)");
+        String menus = Console.readLine().trim();
+        validateMenuInput(menus);
+        return parseMenus(menus, ",");
+    }
+
+    private void validateMenuInput(String menus) {
+        if (menus.matches(MENUS_PATTERN)) {
+            return;
+        }
+        throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+    }
+}

리뷰: 좋은 정규식 하나 배워갑니다!
근데 이렇게 하면 " 메뉴 - 2 " 이렇게 메뉴이름이나 숫자 사이나 앞뒤에 공백이 있어도 invalid하게 판별하고 다시 입력을 요구하나요?
@@ -0,0 +1,42 @@
+package christmas.view;
+
+import static christmas.controller.Parser.parseDate;
+import static christmas.controller.Parser.parseMenus;
+
+import camp.nextstep.edu.missionutils.Console;
+import christmas.exception.ErrorMessage;
+import christmas.exception.PromotionException;
+import java.util.List;
+
+public class InputView {
+    private static final String DATE_PATTERN = "[\\d]{1,2}";
+    private static final String MENUS_PATTERN = "(([가-힣]+)-([\\d]{1,2}),)*([가-힣]+)-([\\d]{1,2})";
+
+    public int readVisitDate() {
+        System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
+        String date = Console.readLine();
+        validateDateInput(date);
+        return parseDate(date);
+    }
+
+    private void validateDateInput(String input) {
+        if (input.matches(DATE_PATTERN)) {
+            return;
+        }
+        throw new PromotionException(ErrorMessage.INVALID_DATE_MESSAGE);
+    }
+
+    public List<String> readMenus() {
+        System.out.println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)");
+        String menus = Console.readLine().trim();
+        validateMenuInput(menus);
+        return parseMenus(menus, ",");
+    }
+
+    private void validateMenuInput(String menus) {
+        if (menus.matches(MENUS_PATTERN)) {
+            return;
+        }
+        throw new PromotionException(ErrorMessage.INVALID_ORDER_MESSAGE);
+    }
+}

리뷰: 맨앞과 맨뒤 글자는 공백이있어도 되지만 메뉴와 - 카운트 사이에는 공백을 허용하지않습니다.
맨앞뒤 공백은 주어진 예시에 공백이 있길래 허용했어요