ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,56 @@
+package christmas.model.constant;
+
+import java.util.Arrays;
+
+public enum Menu {
+    SOUP("애피타이저", "양송이수프", 6000),
+    TAPAS("애피타이저", "타파스", 5500),
+    SALAD("애피타이저", "시저샐러드", 8000),
+    STEAK("메인", "티본스테이크", 55000),
+    BBQ("메인", "바비큐립", 54000),
+    SEAFOOD_PASTA("메인", "해산물파스타", 35000),
+    XMAS_PASTA("메인", "크리스마스파스타", 25000),
+    CHOCO_CAKE("디저트", "초코케이크", 15000),
+    ICECREAM("디저트", "아이스크림", 5000),
+    ZEROCOKE("음료", "제로콜라", 3000),
+    REDWINE("음료", "레드와인", 60000),
+    CHAMPAGNE("음료", "샴페인", 25000);
+
+
+    private final String category;
+    private final String name;
+    private final int price;
+
+    Menu(String category, String name, int price) {
+        this.category = category;
+        this.name = name;
+        this.price = price;
+    }
+
+    public static Menu of(String name) {
+        return Arrays.stream(Menu.values())
+                .filter(menu -> menu.getName().equals(name))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public boolean isMainCategory() {
+        return "메인".equals(this.category);
+    }
+
+    public boolean isDessertCategory() {
+        return "디저트".equals(this.category);
+    }
+
+    public String getCategory() {
+        return category;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+}

리뷰: 음식의 종류도 Enum으로 처리하면 특정 메뉴 종류에 해당되는 음식인지 확인하는 로직을 짤 때 도움이 될 수 있을 것 같아요:)
@@ -0,0 +1,52 @@
+package christmas.model;
+
+import static christmas.exception.CustomException.DATE_RANGE_EXCEPTION;
+import static christmas.model.constant.DateConstant.SPECIAL_DAYS;
+import static christmas.model.constant.DateConstant.WEEKDAYS;
+import static christmas.model.constant.DateConstant.WEEKENDS;
+import static christmas.model.constant.DateConstant.XMAS;
+
+import christmas.model.constant.DateConstant;
+
+public class Date {
+    private final int MIN_DATE = DateConstant.MIN_DATE.getDate();
+    private final int MAX_DATE = DateConstant.MAX_DATE.getDate();
+    private final int date;
+
+    private Date(int date) {
+        validateDateRange(date);
+        this.date = date;
+    }
+
+    public static Date of(int date) {
+        return new Date(date);
+    }
+
+    public int getValue() {
+        return date;
+    }
+
+    private void validateDateRange(int date) {
+        if (date < MIN_DATE || date > MAX_DATE) {
+            throw DATE_RANGE_EXCEPTION;
+        }
+    }
+
+    public boolean isWeekday() {
+        return WEEKDAYS.contains(date);
+    }
+
+    public boolean isWeekend() {
+        return WEEKENDS.contains(date);
+    }
+
+    public boolean isBeforeXmas() {
+        return XMAS.getDate() >= date;
+    }
+
+    public boolean isSpecialDay() {
+        return SPECIAL_DAYS.contains(date);
+    }
+
+
+}

리뷰: 빌트인 객체 Date와 혼동될 수 있어서 다른 네이밍을 반영하는것도 좋을 것 같아요
@@ -21,7 +21,9 @@
 <body>
   <div class="create-account">
     <h1 class="heading xl3">Create <br />Account</h1>
-    <input type="file" id="file_1" class="a11y-hidden" ><label for="file_1" class="input-file"></label>
+    <div class="file-wrapper">
+      <input type="file" id="file_1" class="a11y-hidden" ><label for="file_1" class="input-file"></label>
+    </div>
     <div class="account-info">
       <input type="email" class="input md" placeholder="Email">
       <div class="password">
@@ -43,7 +45,6 @@ <h1 class="heading xl3">Create <br />Account</h1>
     <button class="btn lg3 blue radius-16 btn-done" onclick="goCreate()">Done</button>
     <p class="text md3 btn-cancel" onclick="goCancel()">Cancel</p>
   </div>
-  <!-- <script type="module" src="./js/common.js" defer></script> -->
   <script defer>
     //새로운 페이지 이동하기
     const goCreate = () => {

리뷰: 개인차가 있겠지만, 전 개인적으로 이벤트는 js에서 모두 처리하는편입니다..
react면 컴퍼넌트로 관리되기때문에 무관하지만.. html에선 나중엔 모든페이지 찾아가면서 해당 함수를 제거 또는 추가해야될 경우가 있거든요.
@@ -21,7 +21,9 @@
 <body>
   <div class="create-account">
     <h1 class="heading xl3">Create <br />Account</h1>
-    <input type="file" id="file_1" class="a11y-hidden" ><label for="file_1" class="input-file"></label>
+    <div class="file-wrapper">
+      <input type="file" id="file_1" class="a11y-hidden" ><label for="file_1" class="input-file"></label>
+    </div>
     <div class="account-info">
       <input type="email" class="input md" placeholder="Email">
       <div class="password">
@@ -43,7 +45,6 @@ <h1 class="heading xl3">Create <br />Account</h1>
     <button class="btn lg3 blue radius-16 btn-done" onclick="goCreate()">Done</button>
     <p class="text md3 btn-cancel" onclick="goCancel()">Cancel</p>
   </div>
-  <!-- <script type="module" src="./js/common.js" defer></script> -->
   <script defer>
     //새로운 페이지 이동하기
     const goCreate = () => {

리뷰: 모든 온라인서식에는 title또는 label이 있어야 합니다.
@@ -21,7 +21,9 @@
 <body>
   <div class="create-account">
     <h1 class="heading xl3">Create <br />Account</h1>
-    <input type="file" id="file_1" class="a11y-hidden" ><label for="file_1" class="input-file"></label>
+    <div class="file-wrapper">
+      <input type="file" id="file_1" class="a11y-hidden" ><label for="file_1" class="input-file"></label>
+    </div>
     <div class="account-info">
       <input type="email" class="input md" placeholder="Email">
       <div class="password">
@@ -43,7 +45,6 @@ <h1 class="heading xl3">Create <br />Account</h1>
     <button class="btn lg3 blue radius-16 btn-done" onclick="goCreate()">Done</button>
     <p class="text md3 btn-cancel" onclick="goCancel()">Cancel</p>
   </div>
-  <!-- <script type="module" src="./js/common.js" defer></script> -->
   <script defer>
     //새로운 페이지 이동하기
     const goCreate = () => {

리뷰: 스크립트는 외부파일로..
alert보단 dialog를 이용해서 경고창을 꾸며보는것도 도움됩니다.
기능별로 함수로 구현하셔서 module형 프로그래밍을 시도해보세요~
@@ -1,11 +1,32 @@
-import { useState } from "react";
+import '@styles/App.css';
+import '@styles/reset.css';
+import '@styles/global.css';
+import { Header, Layout, PageRequestError } from '@components/index';
+import { lazy, Suspense } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+
+import { CartItemsContext } from './contexts';
+import { useCartItemIds } from './hooks';
+import ProductListPageSkeleton from './pages/ProductListPage/Skeleton/ProductListPageSkeleton/index.';
+
+const ProductListPage = lazy(() => import('@pages/ProductListPage'));
 
 function App() {
-  const [count, setCount] = useState(0);
+  const { cartItemIds, refreshCartItemIds, error: cartItemsFetchError } = useCartItemIds();
 
+  const cartItemsLength = cartItemIds ? Array.from(cartItemIds).length : 0;
   return (
     <>
-      <h1>React Shopping Products</h1>
+      <Header cartItemsLength={cartItemsLength} cartItemsFetchError={cartItemsFetchError} />
+      <Layout>
+        <ErrorBoundary FallbackComponent={({ error }) => <PageRequestError error={error} />}>
+          <CartItemsContext.Provider value={{ cartItemIds, refreshCartItemIds }}>
+            <Suspense fallback={<ProductListPageSkeleton />}>
+              <ProductListPage />
+            </Suspense>
+          </CartItemsContext.Provider>
+        </ErrorBoundary>
+      </Layout>
     </>
   );
 }

리뷰: 상품의 장바구니 담기/빼기 클릭 시 api요청 후 결과에 맞추어  Header 속 장바구니 아이콘에 장바구니 목록 수량이 업데이트 되어야해서 

장바구니 담기/빼기 + 장바구니 목록 상태를 업데이트해주는 handleCartAction함수를 context api로 내려주고 있어요.

크루는 해당 기능을 어떻게 구현하셨나요?
@@ -1,11 +1,32 @@
-import { useState } from "react";
+import '@styles/App.css';
+import '@styles/reset.css';
+import '@styles/global.css';
+import { Header, Layout, PageRequestError } from '@components/index';
+import { lazy, Suspense } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+
+import { CartItemsContext } from './contexts';
+import { useCartItemIds } from './hooks';
+import ProductListPageSkeleton from './pages/ProductListPage/Skeleton/ProductListPageSkeleton/index.';
+
+const ProductListPage = lazy(() => import('@pages/ProductListPage'));
 
 function App() {
-  const [count, setCount] = useState(0);
+  const { cartItemIds, refreshCartItemIds, error: cartItemsFetchError } = useCartItemIds();
 
+  const cartItemsLength = cartItemIds ? Array.from(cartItemIds).length : 0;
   return (
     <>
-      <h1>React Shopping Products</h1>
+      <Header cartItemsLength={cartItemsLength} cartItemsFetchError={cartItemsFetchError} />
+      <Layout>
+        <ErrorBoundary FallbackComponent={({ error }) => <PageRequestError error={error} />}>
+          <CartItemsContext.Provider value={{ cartItemIds, refreshCartItemIds }}>
+            <Suspense fallback={<ProductListPageSkeleton />}>
+              <ProductListPage />
+            </Suspense>
+          </CartItemsContext.Provider>
+        </ErrorBoundary>
+      </Layout>
     </>
   );
 }

리뷰: 저희도 장바구니 api인 cart-tiems의 데이터는 context로 관리하고 있습니다!
@@ -0,0 +1,39 @@
+import { Filtering, Product, ApiResponse } from '@appTypes/index';
+import { LOAD_MORE_PRODUCTS_AMOUNT } from '@constants/index';
+import { fetchWithToken } from '@utils/index';
+
+import { END_POINTS } from './endPoints';
+
+interface FetchProductParameter {
+  filtering: Filtering;
+  page?: number;
+}
+
+const getSearchParams = ({ filtering, page }: FetchProductParameter): URLSearchParams => {
+  const searchParams = new URLSearchParams();
+
+  if (filtering.category !== '') {
+    searchParams.append('category', filtering.category);
+  }
+
+  searchParams.append('sort', filtering.sort);
+  searchParams.append('page', String(page));
+
+  if (page !== 0) {
+    searchParams.append('size', LOAD_MORE_PRODUCTS_AMOUNT.toString());
+  }
+
+  return searchParams;
+};
+
+export async function fetchProduct(params: FetchProductParameter): Promise<{ products: Product[]; isLast: boolean }> {
+  const searchParams = '?' + getSearchParams({ ...params, page: params.page ?? 0 }).toString();
+
+  const data = await fetchWithToken({
+    url: END_POINTS.products + searchParams,
+    method: 'GET',
+  });
+  const result = (await data.json()) as ApiResponse<Product[]>;
+
+  return { products: result.content, isLast: result.last };
+}

리뷰: page의 타입이 number여서 string으로 만들어 주신 것 같은데, 제가 알기로는 그냥 숫자를 넣어도 된다고 알고 있습니다..! 혹시 string으로 변환하여 넣은 것에 대해 의미가 있는 걸 까요??

(근데 확실하진 않아욧.. 틀리먼 스루 해주십쇼..)

https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams/get
@@ -0,0 +1,15 @@
+export type Category = '' | 'fashion' | 'beverage' | 'electronics' | 'kitchen' | 'fitness' | 'books';
+export type PriceSort = 'price,asc' | 'price,desc';
+
+export interface Product {
+  id: number;
+  name: string;
+  price: number;
+  imageUrl: string;
+  category: Category;
+}
+
+export interface Filtering {
+  category: Category;
+  sort: PriceSort;
+}

리뷰: 빈 문자열이 **전체** 때문에 있는 것으로 보이는데,
Category를 사용하는 곳에서 `Category | undefined`와 같은 형태가 아닌 빈 문자열로 넣어준 이유가 있을까요??
@@ -0,0 +1,32 @@
+/**
+ * 서버 응답의 공통 구조를 정의하는 인터페이스
+ * @template T - response의 content타입
+ */
+export interface ApiResponse<T> {
+  totalPages: number;
+  totalElements: number;
+  sort: {
+    sorted: boolean;
+    unsorted: boolean;
+    empty: boolean;
+  };
+  first: boolean;
+  last: boolean;
+  pageable: {
+    sort: {
+      sorted: boolean;
+      unsorted: boolean;
+      empty: boolean;
+    };
+    pageNumber: number;
+    pageSize: number;
+    paged: boolean;
+    unpaged: boolean;
+    offset: number;
+  };
+  number: number;
+  numberOfElements: number;
+  size: number;
+  content: T;
+  empty: boolean;
+}

리뷰: 오.. 이렇게 따로 분리하는 것도 좋네요~!
@@ -0,0 +1,30 @@
+import { DropdownOption } from '@appTypes/dropdown';
+
+import style from './style.module.css';
+
+interface DropdownProps extends Omit<React.HTMLProps<HTMLSelectElement>, 'onChange'> {
+  options: DropdownOption[];
+  onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
+  label: string;
+}
+
+function Dropdown({ options, label, onChange, name, ...rest }: DropdownProps) {
+  const selectId = `select-${label}`;
+
+  return (
+    <>
+      <label className="scr-only" htmlFor={selectId}>
+        {label}
+      </label>
+      <select id={selectId} name={name} className={style.select} onChange={onChange} {...rest}>
+        {options.map((item) => (
+          <option key={item.value} value={item.value}>
+            {item.label}
+          </option>
+        ))}
+      </select>
+    </>
+  );
+}
+
+export default Dropdown;

리뷰: 하루가 너무 좋아 할 것 같은!
@@ -0,0 +1,15 @@
+export type Category = '' | 'fashion' | 'beverage' | 'electronics' | 'kitchen' | 'fitness' | 'books';
+export type PriceSort = 'price,asc' | 'price,desc';
+
+export interface Product {
+  id: number;
+  name: string;
+  price: number;
+  imageUrl: string;
+  category: Category;
+}
+
+export interface Filtering {
+  category: Category;
+  sort: PriceSort;
+}

리뷰: api 요청 보내는 것과 타입을 맞추었어요.

지금보니, ''를 ALL이라는 상수를 받드는 것이 선언적으로 더 좋을 것 같네요.
@@ -0,0 +1,32 @@
+package christmas.controller;
+
+import christmas.model.Order;
+import christmas.view.OrderOutputPrinter;
+import christmas.view.OutputView;
+
+public class EventPlanner {
+    private static final int MIN_TOTAL_ORDER_AMOUNT_FOR_EVENTS = 10000;
+
+    public static void planEvent(Order order, int date) {
+        int totalOrderAmount = order.calculateTotalOrderAmount();
+
+        printOrderDetails(order, totalOrderAmount);
+
+        processEventBasedOnTotalAmount(order, date, totalOrderAmount);
+    }
+
+    private static void printOrderDetails(Order order, int totalOrderAmount) {
+        OrderOutputPrinter.printMenu(order.getMenus());
+        OutputView.printTotalOrderAmount(totalOrderAmount);
+    }
+
+    private static void processEventBasedOnTotalAmount(Order order, int date, int totalOrderAmount) {
+        if (totalOrderAmount >= MIN_TOTAL_ORDER_AMOUNT_FOR_EVENTS) {
+            EventProcessor.processEvent(order, date, totalOrderAmount);
+        }
+
+        if (totalOrderAmount < MIN_TOTAL_ORDER_AMOUNT_FOR_EVENTS) {
+            NoEventProcessor.processNoEvent(order, date, totalOrderAmount);
+        }
+    }
+}

리뷰: 해당 메서드 및 EventProcessor들은 controller가 아닌 비즈니스 로직에 있어야 할 것 같습니다.
controller의 역할은 view의 값을 model에게 건네주거나, model의 값을 view에게 건네주는 역할로 충분합니다.
@@ -0,0 +1,43 @@
+package christmas.controller;
+
+import christmas.model.Order;
+import christmas.model.OrderItem;
+import christmas.model.TotalDiscountCalculator;
+import christmas.view.DiscountOutputPrinter;
+import christmas.view.OrderOutputPrinter;
+import christmas.view.OutputView;
+import java.util.Set;
+
+public class EventProcessor {
+    public static void processEvent(Order order, int date, int totalOrderAmount) {
+        int totalDiscount = calculateTotalDiscount(order, date);
+        int totalBenefits = TotalDiscountCalculator.calculateGiftMenu(totalOrderAmount, totalDiscount);
+        int totalPayment = TotalDiscountCalculator.calculatePayment(totalOrderAmount, totalDiscount);
+        String giftMenu = OrderOutputPrinter.printGiftMenuMessage(totalOrderAmount);
+
+        OutputView.printGiftMenuPhrase(giftMenu);
+        printDiscountAmount(date, order.getMenus(), totalOrderAmount);
+        printBenefits(totalBenefits);
+        printPayment(totalPayment);
+
+        String badge = TotalDiscountCalculator.calculateBadge(totalOrderAmount, totalDiscount);
+        OutputView.printBadge(badge);
+    }
+
+    public static int calculateTotalDiscount(Order order, int date) {
+        return order.calculateTotalDiscount(date);
+    }
+
+    public static void printDiscountAmount(int date, Set<OrderItem> menus, int totalOrderAmount) {
+        DiscountOutputPrinter.printDiscountAmount(date, menus, totalOrderAmount);
+    }
+
+    public static void printBenefits(int totalBenefits) {
+        DiscountOutputPrinter.printBenefits(totalBenefits);
+    }
+
+    public static void printPayment(int totalPayment) {
+        OutputView.printPayment(totalPayment);
+    }
+
+}

리뷰: 해당 메서드들 정말 분리할 필요가 있었을까요?
@@ -0,0 +1,19 @@
+package christmas.controller;
+
+import christmas.model.Badge;
+import christmas.model.Order;
+import christmas.view.OutputView;
+
+public class NoEventProcessor {
+    public static void processNoEvent(Order order, int date, int totalOrderAmount) {
+        int totalDiscount = 0;
+        int totalBenefits = 0;
+        int totalPayment = totalOrderAmount - totalDiscount;
+
+        EventProcessor.printDiscountAmount(date, order.getMenus(), totalDiscount);
+        EventProcessor.printBenefits(totalBenefits);
+        EventProcessor.printPayment(totalPayment);
+
+        OutputView.printBadge(Badge.NONE.getName());
+    }
+}

리뷰: 해당 값 역시 model에 의해 결정되어야합니다.
그렇게 바꾼다면 Processor 객체들이 필요가 없어지겠네요
@@ -0,0 +1,53 @@
+package christmas.model;
+
+import java.util.Arrays;
+
+public enum Menu {
+    양송이수프(6_000),
+    타파스(5_500),
+    시저샐러드(8_000),
+    티본스테이크(55_000),
+    바비큐립(54_000),
+    해산물파스타(35_000),
+    크리스마스파스타(25_000),
+    초코케이크(15_000),
+    아이스크림(5_000),
+    제로콜라(3_000),
+    레드와인(60_000),
+    샴페인(25_000);
+
+    private final int price;
+
+    Menu(int price) {
+        this.price = price;
+    }
+
+    public static int getChampagnePrice() {
+        return 샴페인.getPrice();
+    }
+
+    public static int getPriceByMenuName(String menuName) {
+        return Menu.valueOf(menuName).getPrice();
+    }
+
+    public static boolean containsMenu(String menuName) {
+        return Arrays.stream(values()).anyMatch(menu -> menu.name().equals(menuName));
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public boolean containsBeverageMenu(String menuName) {
+        return Arrays.asList(제로콜라.name(), 레드와인.name(), 샴페인.name()).contains(menuName);
+    }
+
+    public boolean containsMainMenu(String menuName) {
+        return Arrays.asList(티본스테이크.name(), 바비큐립.name(), 해산물파스타.name(), 크리스마스파스타.name()).contains(menuName);
+    }
+
+    public boolean containsDessertMenu(String menuName) {
+        return Arrays.asList(초코케이크.name(), 아이스크림.name()).contains(menuName);
+    }
+
+}

리뷰: [객체지향생활체조 원칙4. 한 줄에 점을 하나만 찍는다.](https://hudi.blog/thoughtworks-anthology-object-calisthenics/)에 대해 알아볼까요?
@@ -0,0 +1,57 @@
+package christmas.model;
+
+import christmas.view.OutputView;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Order {
+    private static final int MENU_NAME_INDEX = 0;
+    private static final int QUANTITY_INDEX = 1;
+
+    private final Set<OrderItem> menus;
+
+
+    private Order(Set<OrderItem> menus) {
+        this.menus = menus;
+    }
+
+    public static Order createOrder(int date, String orderInput) {
+        Set<OrderItem> menus = createOrderItems(orderInput);
+        OutputView.printOrderPreview(date);
+        return new Order(menus);
+    }
+
+    private static Set<OrderItem> createOrderItems(String orderInput) {
+        Set<OrderItem> menus = new HashSet<>();
+        String[] orderItems = orderInput.split(",");
+        for (String orderItem : orderItems) {
+            String[] menuInfo = getMenuInfo(orderItem.trim());
+            String menuName = menuInfo[MENU_NAME_INDEX];
+            int quantity = Integer.parseInt(menuInfo[QUANTITY_INDEX]);
+            menus.add(new OrderItem(menuName, quantity));
+        }
+        return menus;
+    }
+
+    private static String[] getMenuInfo(String menu) {
+        return menu.split("-");
+    }
+
+    public Set<OrderItem> getMenus() {
+        return menus;
+    }
+
+    public int calculateTotalOrderAmount() {
+        int totalOrderAmount = 0;
+        for (OrderItem orderItem : menus) {
+            totalOrderAmount += orderItem.calculateMenuAmount();
+        }
+        return totalOrderAmount;
+    }
+
+    public int calculateTotalDiscount(int date) {
+        int dateDiscount = TotalDiscountCalculator.calculateDateDiscount(date, menus);
+        int starDiscount = TotalDiscountCalculator.calculateStarDiscount(date);
+        return dateDiscount + starDiscount;
+    }
+}

리뷰: MVC 패턴은 비즈니스 로직(model)과 뷰 로직을 분리하기 위해 사용됩니다.
model 내부에서 view가 사용된다면 mvc 패턴을 잘못 사용하고 있다는 신호입니다
@@ -0,0 +1,57 @@
+package christmas.model;
+
+import christmas.view.OutputView;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Order {
+    private static final int MENU_NAME_INDEX = 0;
+    private static final int QUANTITY_INDEX = 1;
+
+    private final Set<OrderItem> menus;
+
+
+    private Order(Set<OrderItem> menus) {
+        this.menus = menus;
+    }
+
+    public static Order createOrder(int date, String orderInput) {
+        Set<OrderItem> menus = createOrderItems(orderInput);
+        OutputView.printOrderPreview(date);
+        return new Order(menus);
+    }
+
+    private static Set<OrderItem> createOrderItems(String orderInput) {
+        Set<OrderItem> menus = new HashSet<>();
+        String[] orderItems = orderInput.split(",");
+        for (String orderItem : orderItems) {
+            String[] menuInfo = getMenuInfo(orderItem.trim());
+            String menuName = menuInfo[MENU_NAME_INDEX];
+            int quantity = Integer.parseInt(menuInfo[QUANTITY_INDEX]);
+            menus.add(new OrderItem(menuName, quantity));
+        }
+        return menus;
+    }
+
+    private static String[] getMenuInfo(String menu) {
+        return menu.split("-");
+    }
+
+    public Set<OrderItem> getMenus() {
+        return menus;
+    }
+
+    public int calculateTotalOrderAmount() {
+        int totalOrderAmount = 0;
+        for (OrderItem orderItem : menus) {
+            totalOrderAmount += orderItem.calculateMenuAmount();
+        }
+        return totalOrderAmount;
+    }
+
+    public int calculateTotalDiscount(int date) {
+        int dateDiscount = TotalDiscountCalculator.calculateDateDiscount(date, menus);
+        int starDiscount = TotalDiscountCalculator.calculateStarDiscount(date);
+        return dateDiscount + starDiscount;
+    }
+}

리뷰: 매직넘버 상수화 👍👍👍
@@ -0,0 +1,57 @@
+package christmas.model;
+
+import christmas.view.OutputView;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Order {
+    private static final int MENU_NAME_INDEX = 0;
+    private static final int QUANTITY_INDEX = 1;
+
+    private final Set<OrderItem> menus;
+
+
+    private Order(Set<OrderItem> menus) {
+        this.menus = menus;
+    }
+
+    public static Order createOrder(int date, String orderInput) {
+        Set<OrderItem> menus = createOrderItems(orderInput);
+        OutputView.printOrderPreview(date);
+        return new Order(menus);
+    }
+
+    private static Set<OrderItem> createOrderItems(String orderInput) {
+        Set<OrderItem> menus = new HashSet<>();
+        String[] orderItems = orderInput.split(",");
+        for (String orderItem : orderItems) {
+            String[] menuInfo = getMenuInfo(orderItem.trim());
+            String menuName = menuInfo[MENU_NAME_INDEX];
+            int quantity = Integer.parseInt(menuInfo[QUANTITY_INDEX]);
+            menus.add(new OrderItem(menuName, quantity));
+        }
+        return menus;
+    }
+
+    private static String[] getMenuInfo(String menu) {
+        return menu.split("-");
+    }
+
+    public Set<OrderItem> getMenus() {
+        return menus;
+    }
+
+    public int calculateTotalOrderAmount() {
+        int totalOrderAmount = 0;
+        for (OrderItem orderItem : menus) {
+            totalOrderAmount += orderItem.calculateMenuAmount();
+        }
+        return totalOrderAmount;
+    }
+
+    public int calculateTotalDiscount(int date) {
+        int dateDiscount = TotalDiscountCalculator.calculateDateDiscount(date, menus);
+        int starDiscount = TotalDiscountCalculator.calculateStarDiscount(date);
+        return dateDiscount + starDiscount;
+    }
+}

리뷰: `"-"` 해당 값도 상수화 해주면 더 좋을 것 같습니다
@@ -0,0 +1,23 @@
+package christmas.model;
+
+public class OrderItem {
+    private final String menuName;
+    private final int quantity;
+
+    public OrderItem(String menuName, int quantity) {
+        this.menuName = menuName;
+        this.quantity = quantity;
+    }
+
+    public String getMenuName() {
+        return menuName;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public int calculateMenuAmount() {
+        return quantity * Menu.getPriceByMenuName(menuName);
+    }
+}

리뷰: 보통 getter 메서드는 자바 컨벤션상 가장 아래쪽에 위치합니다