ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,77 @@
+export const validInputDates = ["0", "1", "31", "32", "-1", "-31"];
+
+export const invalidInputDates = [
+  "-0",
+  "2.5",
+  "foo",
+  "bar",
+  "😊",
+  "12.0",
+  "12.000000000000004",
+  "12345678910987654321",
+];
+
+export const validDomainDates = [1, 2, 11, 15, 23, 31];
+
+export const invalidDomainDates = [32, 0];
+
+export const validInputMenus = [
+  "-1",
+  "티본스테이크-1",
+  "양송이수프-1,티본스테이크-0",
+  "티본스테이크-30",
+  "레드와인-1",
+  "레드와인-1,블루와인-1,퍼플와인-1,그린와인-1",
+  "foo-1",
+];
+
+export const invalidInputMenus = [
+  "티본스테이크1",
+  "양송이수프-1,티본-스테이크-1",
+  "시저샐러드-1,티본스테이크--1",
+  "티본스테이크-foo",
+  "티본스테이크-1.0",
+  "티본스테이크-1,티본스테이크-1",
+  "티본스테이크-1.000000000000004",
+  "티본스테이크-1.시저샐러드-1",
+  "티본스테이크-1시저샐러드-1",
+  "티본스테이크-1,,시저샐러드-1",
+  "티본스테이크-1,-,시저샐러드-1",
+];
+
+export const valiidDomainMenus = [
+  [["티본스테이크-1"]],
+  [
+    [
+      "양송이수프-3",
+      "타파스-2",
+      "시저샐러드-3",
+      "티본스테이크-1",
+      "해산물파스타-2",
+      "아이스크림-3",
+      "제로콜라-6",
+    ],
+  ],
+];
+
+export const invalidDomainMenus = [
+  [["-1"]],
+  [["티본스테이크-1", "-1", "시저샐러드-1"]],
+  [["foo-1"]],
+  [["티본스태이크-1"]],
+  [["티본스테이크-0"]],
+  [["샴페인-1", "제로콜라-2", "레드와인-1"]],
+  [["제로콜라-15"]],
+  [["티본스테이크-10", "타파스-10", "해산물파스타-1"]],
+  [
+    [
+      "양송이수프-3",
+      "타파스-2",
+      "시저샐러드-5",
+      "티본스테이크-3",
+      "해산물파스타-2",
+      "아이스크림-5",
+      "제로콜라-1",
+    ],
+  ],
+];

리뷰: `Jest`에 `--verbose`라는 옵션이 있는지 몰랐네요. 그런데 찾아보니 `--verbose`는 테스트에 사용된 값들을 보여주는 게 아니라 테스트 결과를 계층 구조로 보여주는 옵션인 것 같아요. 혹시 제가 잘못 이해한 것이라면 알려주세요!

---

### 테스트 코드의 목적
제가 리뷰드린 의도는 "어떤 값으로 테스트를 진행하는지를 **테스트 코드 내부** 에서 확인하고 관리해야 코드를 이해하고 유지보수하기 쉽지 않을까?"라는 뜻이었습니다.
물론 지금처럼 테스트 코드와 테스트에 사용되는 값들을 따로 분리하여 저장해도 마우스 올리면 값이 다 나오긴 하지만, 코드를 딱 봤을 때 어떤 값이 예외를 일으키는지, 또 정상인지, 어떤 케이스들이 어떤 결과를 내는지 알 수 있게끔 문서 역할도 수행하는 게 테스트 코드라고 생각해요. 그래서 테스트 코드 내부에서 값들이 관리되어야 하지 않을까 생각해봅니다.

### 공통적으로 사용되는 값을 추출하는 것의 장단점
말씀하신 것처럼 여러 군데에서 동일하게 사용되는 값들이 있을 때, 프로덕션 코드에서 추출하듯 테스트 코드에서도 추출해야 하는지 고민이 되긴 하네요🤔. 동일한 값을 사용하는 테스트가 많아질수록 추출하는 게 더 편할 수도 있을 것 같구요.
그런데 A,B,C ... 여러 개의 테스트가 "Z" 라는 테스트 케이스를 사용하는데, A 테스트가 모종의 이유로 해당 테스트 케이스에서 딱 하나만 빼고 사용하게 된다고 하면 골치아파질 것 같아요.
예를 들면 "Z" 테스트 케이스가 양의 정수가 아닌 값들의 모음일 때, "Z"에는 소수 값이 있을 수 있는데요. 그럼 A,B,C ... 여러 개의 테스트는 "Z"를 뽑아서 사용할 것입니다. 그런데 A 라는 기능이 모종의 이유로 인해 소수가 들어와도 되도록 변경된다면, "Z" 케이스를 지금처럼 쏙 뽑아다 쓸 수 없을 것 같아요.

---

저도 무엇이 정답인지 잘 모르겠습니다😅. 저희가 작은 규모의 프로젝트에만 테스트 코드를 적용해봐서 헷갈려하는 걸수도 있다는 생각도 들구요. 테스트 코드에 관한 책을 읽어보거나 점점 큰 규모에 적용시켜본다면 좋은 방식에 대한 확신을 가질 수 있을 것 같아요.
@@ -0,0 +1,77 @@
+export const validInputDates = ["0", "1", "31", "32", "-1", "-31"];
+
+export const invalidInputDates = [
+  "-0",
+  "2.5",
+  "foo",
+  "bar",
+  "😊",
+  "12.0",
+  "12.000000000000004",
+  "12345678910987654321",
+];
+
+export const validDomainDates = [1, 2, 11, 15, 23, 31];
+
+export const invalidDomainDates = [32, 0];
+
+export const validInputMenus = [
+  "-1",
+  "티본스테이크-1",
+  "양송이수프-1,티본스테이크-0",
+  "티본스테이크-30",
+  "레드와인-1",
+  "레드와인-1,블루와인-1,퍼플와인-1,그린와인-1",
+  "foo-1",
+];
+
+export const invalidInputMenus = [
+  "티본스테이크1",
+  "양송이수프-1,티본-스테이크-1",
+  "시저샐러드-1,티본스테이크--1",
+  "티본스테이크-foo",
+  "티본스테이크-1.0",
+  "티본스테이크-1,티본스테이크-1",
+  "티본스테이크-1.000000000000004",
+  "티본스테이크-1.시저샐러드-1",
+  "티본스테이크-1시저샐러드-1",
+  "티본스테이크-1,,시저샐러드-1",
+  "티본스테이크-1,-,시저샐러드-1",
+];
+
+export const valiidDomainMenus = [
+  [["티본스테이크-1"]],
+  [
+    [
+      "양송이수프-3",
+      "타파스-2",
+      "시저샐러드-3",
+      "티본스테이크-1",
+      "해산물파스타-2",
+      "아이스크림-3",
+      "제로콜라-6",
+    ],
+  ],
+];
+
+export const invalidDomainMenus = [
+  [["-1"]],
+  [["티본스테이크-1", "-1", "시저샐러드-1"]],
+  [["foo-1"]],
+  [["티본스태이크-1"]],
+  [["티본스테이크-0"]],
+  [["샴페인-1", "제로콜라-2", "레드와인-1"]],
+  [["제로콜라-15"]],
+  [["티본스테이크-10", "타파스-10", "해산물파스타-1"]],
+  [
+    [
+      "양송이수프-3",
+      "타파스-2",
+      "시저샐러드-5",
+      "티본스테이크-3",
+      "해산물파스타-2",
+      "아이스크림-5",
+      "제로콜라-1",
+    ],
+  ],
+];

리뷰: 음 이 부분은 제가 오해하기도 했고, 또 설명이 좀 불충분했는데요... 

우선 verbose를 쓰고 인자에 넘길 때, 전체를 넘기는 것이 아니라 점표기법으로 특정 필드를 특정해서 넘기게 된다면 테스트를 '돌렸을 때' 어떤 값이 테스트가 되는지 알 수 있을 것이란 말이었습니다. 물론 이렇게 한다고 하더라도 '테스트 코드'에는 직접적으로 어떤 값이 들어오는지 알 수는 없지만요. 말씀해주신 문제가 어느 정도 해소가 되리라는 생각이었습니다.

물론 `문서`의 역할을 해야 한다면 이 테스트 코드는 어떤 부분에서 기능을 못하고 있는 것이 사실입니다. 이 부분은 깔끔하게 인정할 수밖에 없겠네요.

하지만 케이스가 특수하게 바뀌는 상황이라면 테스트 케이스를 몰아서 관리하는 편이 확실히 편리하다고 생각합니다. 만약 `평일 할인`의 대상이 되는 품목에 어떤 이유에서인지 `제로콜라`가 추가 되었다고 하면, (넘기는 인자의 종류에 따라 다르겠지만) 통상적인 방법으로는 1. 평일 할인 계산 테스트, 2. 전체 할인 계산 테스트, 3. 혜택 계산 테스트, 4. 배지 테스트가 영향을 받게 됩니다. 결국 이 네 개의 테스트 코드를 찾아서 제로 콜라 관련 케이스를 다 수정해야 하는 반면, 테스트 케이스를 몰아서 관리하게 되면 `제로 콜라` 케이스를 찾은 다음 저 4개의 내용을 고쳐주기만 하면 됩니다. 

만약 쓱 빼는 것이 힘들다는 것이 문제라면 아마도 테스트 케이스마다 허용되는 시나리오나 수준을 필드로 정해줄 수도 있겠죠? 그러면 A에서 테스트할 때 Z에서 `inputLevel`을 제외하고 필터하거나 `domainLevel`인 것들만 필터를 해서 테스트를 할 수 있으니 나름의 방법이 될 것 같기도 합니다.

사실 이런 문제 때문에 지금도 `inputCase`와 `validCase` 그리고 `tryErrorCase`라는 3가지 별도의 파일을 통일된 형식 없이 운용하고 있었는데요. 말씀해주신 부분을 생각하고 보니 조금 더 통일성 있는 형식으로 사용하면서도 또 케이스를 빼서 쓰기 어려운 상황들이 나뉘는 부분도 조금은 해결할 길이 보이는 것 같습니다.

물론 이런 방법이 그리 깔끔하지는 않지만 말이죠. 그래도 말씀해주신 부분이 많은 도움이 되었습니다.
앞으로 현장에서 해나가면서 무엇이 어떤 상황에서 더 좋은지, 또 어떤 장점과 단점이 있는지 더 배우게 되겠죠. 덕분에 그런 배움의 시간이 왔을 때 조금 더 열린 사람이 될 수 있을 것 같습니다.
@@ -0,0 +1,61 @@
+package christmas.controller;
+
+import christmas.domain.VisitDate;
+import christmas.domain.order.Order;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+import java.util.function.Function;
+
+public class Controller {
+
+	private VisitDate visitDate;
+	private Order order;
+	private final OutputView outputView = new OutputView();
+	private final InputView inputView = new InputView();
+
+	public void run() {
+		askVisitDate();
+
+		askOrderMenu();
+
+		showPreviewBenefit();
+	}
+
+
+	private void askVisitDate() {
+		outputView.askVisitDate();
+
+		String userInput = getValidInput(VisitDate::new);
+
+		visitDate = new VisitDate(userInput);
+	}
+
+	private void askOrderMenu() {
+		outputView.askOrderMenu();
+
+		String userInput = getValidInput(Order::new);
+
+		order = new Order(userInput);
+	}
+
+	private void showPreviewBenefit() {
+		outputView.showBenefit();
+		outputView.showPreviewBenefit(order, visitDate);
+	}
+
+	private <T> String getValidInput(Function<String, T> constructor) {
+		String input;
+
+		do {
+			input = inputView.getUserInput().trim();
+			try {
+				constructor.apply(input);
+			} catch (IllegalArgumentException e) {
+				outputView.printExceptionMessage(e.getMessage());
+				input = null;
+			}
+		} while (input == null);
+
+		return input;
+	}
+}
\ No newline at end of file

리뷰: Function 같이 함수를 전달하는 방법으로 옳은 값을 받을 때까지 반복했군요!
저는 옳은 값을 받은 때까지 다시 받는 기능을 그저 날짜와 메뉴, 두 곳에서 중복 구현했었습니다. 그 뒤에 함수형 프로그래밍에 대해 접해보긴 했지만 잘 와닿지 않았는데 
이 코드를 보니까 이해가 되는 것 같아요
@@ -0,0 +1,30 @@
+package christmas.config;
+
+import christmas.config.menu.Beverage;
+import christmas.config.menu.Menu;
+
+public enum Present {
+	CHAMPAGNE(120_000, Beverage.CHAMPAGNE, 1);
+
+	private final int baseAmount;
+	private final Menu menu;
+	private final int presentCount;
+
+	Present(int baseAmount, Menu menu, int presentCount) {
+		this.baseAmount = baseAmount;
+		this.menu = menu;
+		this.presentCount = presentCount;
+	}
+
+	public int getBaseAmount() {
+		return baseAmount;
+	}
+
+	public Menu getMenu() {
+		return menu;
+	}
+
+	public int getPresentCount() {
+		return presentCount;
+	}
+}
\ No newline at end of file

리뷰: 메뉴를 클래스로 정의하는 방식이 사람마다 다른게 신기하네요! 저는 메뉴 타입일 구현하고 메뉴 타입을 가진 메뉴들을 전부 Menu enum 클래스에 넣었습니다. 
근데 Present에 기준 금액이 있다는 점이 눈에 띕니다. 이 Present 클래스는 "기준 금액을 넘으면 받는 이벤트들의 선물 목록"으로 보입니다. 만약 기준 금액을 넘는게 아니라 다른 방식으로 선물을 지급 받는 이벤트가 있다면 다른 클래스를 구현해야할 것 같습니다.
@@ -0,0 +1,76 @@
+package christmas.domain;
+
+import static christmas.config.message.ErrorMessage.INVALID_DATE;
+import static christmas.domain.constant.DateConstant.CHRISTMAS_D_DAY_DISCOUNT_END_DATE;
+import static christmas.domain.constant.DateConstant.CHRISTMAS_D_DAY_DISCOUNT_START_DATE;
+import static christmas.domain.constant.DateConstant.MONTH;
+import static christmas.domain.constant.DateConstant.SPECIAL_DAYS;
+import static christmas.domain.constant.DateConstant.WEEKDAYS;
+import static christmas.domain.constant.DateConstant.WEEKEND;
+import static christmas.domain.constant.DateConstant.YEARS;
+
+import christmas.validator.InputValidator;
+import christmas.validator.NumberValidator;
+import java.time.DateTimeException;
+import java.time.LocalDate;
+
+public class VisitDate {
+
+	private final int date;
+
+	public VisitDate(String date) {
+		validate(date);
+
+		this.date = convertToInt(date);
+	}
+
+	private void validate(String date) {
+		if (!NumberValidator.isNumeric(date)) {
+			throw new IllegalArgumentException(INVALID_DATE.getMessage());
+		}
+
+		if (InputValidator.isBlank(date)) {
+			throw new IllegalArgumentException(INVALID_DATE.getMessage());
+		}
+
+		if (!isValidDay(date)) {
+			throw new IllegalArgumentException(INVALID_DATE.getMessage());
+		}
+	}
+
+	private static boolean isValidDay(String date) {
+		try {
+			int day = Integer.parseInt(date);
+			LocalDate.of(YEARS, MONTH, day);
+			return true;
+		} catch (DateTimeException e) {
+			return false;
+		}
+	}
+
+	private int convertToInt(String date) {
+		return Integer.parseInt(date);
+	}
+
+	public boolean isWeekday() {
+		LocalDate day = LocalDate.of(YEARS, MONTH, date);
+		return WEEKDAYS.contains(day.getDayOfWeek());
+	}
+
+	public boolean isWeekend() {
+		LocalDate day = LocalDate.of(YEARS, MONTH, date);
+		return WEEKEND.contains(day.getDayOfWeek());
+	}
+
+	public boolean isSpecialDate() {
+		return SPECIAL_DAYS.contains(date);
+	}
+
+	public boolean isChristmasDdayPeriod() {
+		return CHRISTMAS_D_DAY_DISCOUNT_START_DATE <= date && date <= CHRISTMAS_D_DAY_DISCOUNT_END_DATE;
+	}
+
+	public int getChristmasDiscountAccrueDate() {
+		return date - 1;
+	}
+}
\ No newline at end of file

리뷰: 평일 / 주말 을 확인하는데 LocalDate 라이브러리를 이용하는 쉬운 방법이 있는 줄 몰랐습니다.
그것도 모르고 시작 날짜, 끝 날짜의 요일을 구하고 계산하고.. 쉬운 방법두고 고전했었네요
@@ -0,0 +1,30 @@
+package christmas.config;
+
+import christmas.config.menu.Beverage;
+import christmas.config.menu.Menu;
+
+public enum Present {
+	CHAMPAGNE(120_000, Beverage.CHAMPAGNE, 1);
+
+	private final int baseAmount;
+	private final Menu menu;
+	private final int presentCount;
+
+	Present(int baseAmount, Menu menu, int presentCount) {
+		this.baseAmount = baseAmount;
+		this.menu = menu;
+		this.presentCount = presentCount;
+	}
+
+	public int getBaseAmount() {
+		return baseAmount;
+	}
+
+	public Menu getMenu() {
+		return menu;
+	}
+
+	public int getPresentCount() {
+		return presentCount;
+	}
+}
\ No newline at end of file

리뷰: 그렇네요! 전 증정품이라는게 일정금액 이상일 경우에만 지급이 가능하다고 생각을 했는데! 만약 다른 방식일 경우는 또 수정이 필요하겠네요
@@ -0,0 +1,23 @@
+package christmas.config;
+
+public enum Badge {
+	STAR(5_000, "별"),
+	TREE(10_000, "트리"),
+	SANTA(20_000, "산타");
+
+	private final int baseAmount;
+	private final String name;
+
+	Badge(int baseAmount, String name) {
+		this.baseAmount = baseAmount;
+		this.name = name;
+	}
+
+	public int getBaseAmount() {
+		return baseAmount;
+	}
+
+	public String getName() {
+		return name;
+	}
+}
\ No newline at end of file

리뷰: (궁금해요!)

config 패키지의 역할이 무엇인가요?
상수를 관리한다면 domain의 constant와 다른점이 무엇인가요!
@@ -0,0 +1,18 @@
+package christmas.config.message;
+
+import static christmas.domain.constant.MenuConstant.MAXIMUM_ORDER_QUANTITY;
+
+public enum ErrorMessage {
+	INVALID_DATE("[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."),
+	INVALID_ORDER("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."),
+	EXCEEDED_ORDER_QUANTITY_LIMIT(String.format("[ERROR] 메뉴는 한 번에 최대 %d개까지만 주문할 수 있습니다.", MAXIMUM_ORDER_QUANTITY));
+	private final String message;
+
+	ErrorMessage(String message) {
+		this.message = message;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+}
\ No newline at end of file

리뷰: (추천드려요!) 

[ERROR] 혹은 `다시 입력해 주세요` 는 prefix, suffix로 분리가 가능할 것 같아요 :)
@@ -0,0 +1,71 @@
+package christmas.config.message;
+
+import christmas.util.NumberFormatter;
+
+public enum ResultMessage {
+	ORDER_MENU("<주문 메뉴>"),
+	TOTAL_ORDER_AMOUNT_BEFORE_DISCOUNT("<할인 전 총주문 금액>"),
+	GIFT_MENU("<증정 메뉴>"),
+	BENEFIT_DETAILS("<혜택 내역>"),
+	TOTAL_BENEFIT_AMOUNT("<총혜택 금액>"),
+	ESTIMATED_PAYMENT_AFTER_DISCOUNT("<할인 후 예상 결제 금액>"),
+	DECEMBER_EVENT_BADGE("<12월 이벤트 배지>"),
+	NONE("없음"),
+	MONETARY_UNIT("원"),
+	ORDER_MENU_FORMAT("%s %s개"),
+	BENEFIT_FORMAT("%s: -%s원"),
+	TOTAL_BENEFIT_FORMAT("-%s원"),
+	NEW_LINE("\n");
+
+	private final String message;
+
+	ResultMessage(String message) {
+		this.message = message;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public static String getOrderMenuFormat(String orderMenuList) {
+		return NEW_LINE.getMessage() + ORDER_MENU.getMessage() + NEW_LINE.getMessage() + orderMenuList;
+	}
+
+	public static String getTotalOrderAmountFormat(int totalOrderAmount) {
+		return TOTAL_ORDER_AMOUNT_BEFORE_DISCOUNT.getMessage() + NEW_LINE.getMessage()
+			+ NumberFormatter.getNumberFormat(totalOrderAmount) + MONETARY_UNIT.getMessage() + NEW_LINE.getMessage();
+	}
+
+	public static String getGiftFormat(String gifts) {
+		return GIFT_MENU.getMessage() + NEW_LINE.getMessage() + gifts + NEW_LINE.getMessage();
+	}
+
+	public static String getTotalBenefitAmountFormat(String totalBenefitAmount) {
+		return TOTAL_BENEFIT_AMOUNT.getMessage() + NEW_LINE.getMessage() + totalBenefitAmount + NEW_LINE.getMessage();
+	}
+
+	public static String getFinalPaymentAmountFormat(int finalPaymentAmount) {
+		return ESTIMATED_PAYMENT_AFTER_DISCOUNT.getMessage() + NEW_LINE.getMessage()
+			+ NumberFormatter.getNumberFormat(finalPaymentAmount) + MONETARY_UNIT.getMessage() + NEW_LINE.getMessage();
+	}
+
+	public static String getBadgeEventFormat(String badge) {
+		return DECEMBER_EVENT_BADGE.getMessage() + NEW_LINE.getMessage() + badge;
+	}
+
+	public static String getOrderMenuFormat(String menuName, int count) {
+		return String.format(ORDER_MENU_FORMAT.getMessage(), menuName, NumberFormatter.getNumberFormat(count)) + NEW_LINE.getMessage();
+	}
+
+	public static String getPresentFormat(String menuName, int count) {
+		return String.format(ORDER_MENU_FORMAT.getMessage(), menuName, NumberFormatter.getNumberFormat(count)) + NEW_LINE.getMessage();
+	}
+
+	public static String getBenefitFormat(String eventName, int discountAmount) {
+		return String.format(BENEFIT_FORMAT.getMessage(), eventName, NumberFormatter.getNumberFormat(discountAmount));
+	}
+
+	public static String getTotalBenefitFormat(int totalBenefitAmount) {
+		return String.format(TOTAL_BENEFIT_FORMAT.getMessage(), NumberFormatter.getNumberFormat(totalBenefitAmount));
+	}
+}
\ No newline at end of file

리뷰: (궁금해요)

코드를 보다가 아래 상황에서 출력 메시지를 상수로 분리하면 얻을 수 있는 이점이 무엇인가요?
- 한 번만 사용하는 문자열의 경우
- `없음` 과 `원`, `\n` 은 분리하지 않아도 어떤 의미를 가지는지 이해가는 경우

(이런 방법도 있어요!)
만약 0과 같은 값이 생긴다면 `-0`으로 표시 될 가능성이 있기 때문에 BENEFIT_FORMAT의 금액은 -1을 곱하는 것도 하나의 방법이라고 생각합니다!!
@@ -0,0 +1,30 @@
+package christmas.config.menu;
+
+public enum Appetizer implements Menu {
+	BUTTON_MUSHROOM_SOUP("양송이수프", 6_000),
+	TAPAS("타파스", 5_500),
+	CAESAR_SALAD("시저샐러드", 8_000);
+
+	private final String name;
+	private final int price;
+
+	Appetizer(String name, int price) {
+		this.name = name;
+		this.price = price;
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	@Override
+	public int getPrice() {
+		return price;
+	}
+
+	@Override
+	public Category getCategory() {
+		return Category.APPETIZER;
+	}
+}
\ No newline at end of file

리뷰: 전 그냥 단순히 6000 으로 적었는데 언더바를 이용해서도 작성할 수 있군요,, 처음 알았습니다..! 숫자가 길 땐 확실히 더 좋은 표현 같아요! 아직 자바에 대해 서툴러서 배우고갑니다!
@@ -0,0 +1,61 @@
+package christmas.controller;
+
+import christmas.domain.VisitDate;
+import christmas.domain.order.Order;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+import java.util.function.Function;
+
+public class Controller {
+
+	private VisitDate visitDate;
+	private Order order;
+	private final OutputView outputView = new OutputView();
+	private final InputView inputView = new InputView();
+
+	public void run() {
+		askVisitDate();
+
+		askOrderMenu();
+
+		showPreviewBenefit();
+	}
+
+
+	private void askVisitDate() {
+		outputView.askVisitDate();
+
+		String userInput = getValidInput(VisitDate::new);
+
+		visitDate = new VisitDate(userInput);
+	}
+
+	private void askOrderMenu() {
+		outputView.askOrderMenu();
+
+		String userInput = getValidInput(Order::new);
+
+		order = new Order(userInput);
+	}
+
+	private void showPreviewBenefit() {
+		outputView.showBenefit();
+		outputView.showPreviewBenefit(order, visitDate);
+	}
+
+	private <T> String getValidInput(Function<String, T> constructor) {
+		String input;
+
+		do {
+			input = inputView.getUserInput().trim();
+			try {
+				constructor.apply(input);
+			} catch (IllegalArgumentException e) {
+				outputView.printExceptionMessage(e.getMessage());
+				input = null;
+			}
+		} while (input == null);
+
+		return input;
+	}
+}
\ No newline at end of file

리뷰: (궁금해요!)

처음에 `getValidInput(VisitDate::new)` 코드를 봤을 때 `VisitDate`가 반환될 것 같다고 생각했습니다.
하지만 문자열이 반환되었고 내부 코드를 봤을 때 VisitDate를 생성하는 과정에서 검증 과정을 거치고 마지막에 입력한 문자열을 다시 반환하는 코드였습니다.
그렇다면 이미 검증이 완료된 값을 사용해도 충분할 것 같다고 생각하는데 다시 한번 더 생성하는 이유가 있나요?
@@ -0,0 +1,6 @@
+package christmas.domain.amount;
+
+public interface Amount {
+
+	int getAmount();
+}
\ No newline at end of file

리뷰: (개인적인 생각입니다!) 

Amount를 추상 클래스로 적용해도 괜찮을 것 같습니다 :)
@@ -0,0 +1,76 @@
+package christmas.domain;
+
+import static christmas.config.message.ErrorMessage.INVALID_DATE;
+import static christmas.domain.constant.DateConstant.CHRISTMAS_D_DAY_DISCOUNT_END_DATE;
+import static christmas.domain.constant.DateConstant.CHRISTMAS_D_DAY_DISCOUNT_START_DATE;
+import static christmas.domain.constant.DateConstant.MONTH;
+import static christmas.domain.constant.DateConstant.SPECIAL_DAYS;
+import static christmas.domain.constant.DateConstant.WEEKDAYS;
+import static christmas.domain.constant.DateConstant.WEEKEND;
+import static christmas.domain.constant.DateConstant.YEARS;
+
+import christmas.validator.InputValidator;
+import christmas.validator.NumberValidator;
+import java.time.DateTimeException;
+import java.time.LocalDate;
+
+public class VisitDate {
+
+	private final int date;
+
+	public VisitDate(String date) {
+		validate(date);
+
+		this.date = convertToInt(date);
+	}
+
+	private void validate(String date) {
+		if (!NumberValidator.isNumeric(date)) {
+			throw new IllegalArgumentException(INVALID_DATE.getMessage());
+		}
+
+		if (InputValidator.isBlank(date)) {
+			throw new IllegalArgumentException(INVALID_DATE.getMessage());
+		}
+
+		if (!isValidDay(date)) {
+			throw new IllegalArgumentException(INVALID_DATE.getMessage());
+		}
+	}
+
+	private static boolean isValidDay(String date) {
+		try {
+			int day = Integer.parseInt(date);
+			LocalDate.of(YEARS, MONTH, day);
+			return true;
+		} catch (DateTimeException e) {
+			return false;
+		}
+	}
+
+	private int convertToInt(String date) {
+		return Integer.parseInt(date);
+	}
+
+	public boolean isWeekday() {
+		LocalDate day = LocalDate.of(YEARS, MONTH, date);
+		return WEEKDAYS.contains(day.getDayOfWeek());
+	}
+
+	public boolean isWeekend() {
+		LocalDate day = LocalDate.of(YEARS, MONTH, date);
+		return WEEKEND.contains(day.getDayOfWeek());
+	}
+
+	public boolean isSpecialDate() {
+		return SPECIAL_DAYS.contains(date);
+	}
+
+	public boolean isChristmasDdayPeriod() {
+		return CHRISTMAS_D_DAY_DISCOUNT_START_DATE <= date && date <= CHRISTMAS_D_DAY_DISCOUNT_END_DATE;
+	}
+
+	public int getChristmasDiscountAccrueDate() {
+		return date - 1;
+	}
+}
\ No newline at end of file

리뷰: (궁금해요)

문자열이 공백검사 코드가 정수 검증 코드보다 앞에 오는 것이 순서상 올바르다고 생각했는데 혹시 어떻게 생각하시는지 궁금합니다 :)
@@ -0,0 +1,71 @@
+package christmas.config.message;
+
+import christmas.util.NumberFormatter;
+
+public enum ResultMessage {
+	ORDER_MENU("<주문 메뉴>"),
+	TOTAL_ORDER_AMOUNT_BEFORE_DISCOUNT("<할인 전 총주문 금액>"),
+	GIFT_MENU("<증정 메뉴>"),
+	BENEFIT_DETAILS("<혜택 내역>"),
+	TOTAL_BENEFIT_AMOUNT("<총혜택 금액>"),
+	ESTIMATED_PAYMENT_AFTER_DISCOUNT("<할인 후 예상 결제 금액>"),
+	DECEMBER_EVENT_BADGE("<12월 이벤트 배지>"),
+	NONE("없음"),
+	MONETARY_UNIT("원"),
+	ORDER_MENU_FORMAT("%s %s개"),
+	BENEFIT_FORMAT("%s: -%s원"),
+	TOTAL_BENEFIT_FORMAT("-%s원"),
+	NEW_LINE("\n");
+
+	private final String message;
+
+	ResultMessage(String message) {
+		this.message = message;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public static String getOrderMenuFormat(String orderMenuList) {
+		return NEW_LINE.getMessage() + ORDER_MENU.getMessage() + NEW_LINE.getMessage() + orderMenuList;
+	}
+
+	public static String getTotalOrderAmountFormat(int totalOrderAmount) {
+		return TOTAL_ORDER_AMOUNT_BEFORE_DISCOUNT.getMessage() + NEW_LINE.getMessage()
+			+ NumberFormatter.getNumberFormat(totalOrderAmount) + MONETARY_UNIT.getMessage() + NEW_LINE.getMessage();
+	}
+
+	public static String getGiftFormat(String gifts) {
+		return GIFT_MENU.getMessage() + NEW_LINE.getMessage() + gifts + NEW_LINE.getMessage();
+	}
+
+	public static String getTotalBenefitAmountFormat(String totalBenefitAmount) {
+		return TOTAL_BENEFIT_AMOUNT.getMessage() + NEW_LINE.getMessage() + totalBenefitAmount + NEW_LINE.getMessage();
+	}
+
+	public static String getFinalPaymentAmountFormat(int finalPaymentAmount) {
+		return ESTIMATED_PAYMENT_AFTER_DISCOUNT.getMessage() + NEW_LINE.getMessage()
+			+ NumberFormatter.getNumberFormat(finalPaymentAmount) + MONETARY_UNIT.getMessage() + NEW_LINE.getMessage();
+	}
+
+	public static String getBadgeEventFormat(String badge) {
+		return DECEMBER_EVENT_BADGE.getMessage() + NEW_LINE.getMessage() + badge;
+	}
+
+	public static String getOrderMenuFormat(String menuName, int count) {
+		return String.format(ORDER_MENU_FORMAT.getMessage(), menuName, NumberFormatter.getNumberFormat(count)) + NEW_LINE.getMessage();
+	}
+
+	public static String getPresentFormat(String menuName, int count) {
+		return String.format(ORDER_MENU_FORMAT.getMessage(), menuName, NumberFormatter.getNumberFormat(count)) + NEW_LINE.getMessage();
+	}
+
+	public static String getBenefitFormat(String eventName, int discountAmount) {
+		return String.format(BENEFIT_FORMAT.getMessage(), eventName, NumberFormatter.getNumberFormat(discountAmount));
+	}
+
+	public static String getTotalBenefitFormat(int totalBenefitAmount) {
+		return String.format(TOTAL_BENEFIT_FORMAT.getMessage(), NumberFormatter.getNumberFormat(totalBenefitAmount));
+	}
+}
\ No newline at end of file

리뷰: 출력형식에 맞는 String을 반환하는 메서드같아요! 저는 이런 역할을 View에 부여했는데 여기서 작성하신 이유가 궁금해요!
@@ -0,0 +1,26 @@
+package christmas.domain;
+
+import christmas.config.menu.Appetizer;
+import christmas.config.menu.Beverage;
+import christmas.config.menu.Dessert;
+import christmas.config.menu.MainDish;
+import christmas.config.menu.Menu;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MenuCategory {
+
+	private final List<Menu> menuCategory = new ArrayList<>();
+
+	public MenuCategory() {
+		menuCategory.addAll(Arrays.asList(Appetizer.values()));
+		menuCategory.addAll(Arrays.asList(Beverage.values()));
+		menuCategory.addAll(Arrays.asList(Dessert.values()));
+		menuCategory.addAll(Arrays.asList(MainDish.values()));
+	}
+
+	public List<Menu> getMenuCategory() {
+		return menuCategory;
+	}
+}
\ No newline at end of file

리뷰: Order에서 메뉴 카테고리를 참조하기 위해서 MenuCategory라는 클래스를 만드신 것 같아요! MenuCategory안에 모든 메뉴들이 담겨있는 것 같은데 MenuCategory 클래스 대신 Menu에서 맞는 카테고리를 반환하는 메서드가 있는건 어떨까요??