ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,91 @@
+import { useRecoilValue } from "recoil";
+import styled from "styled-components";
+import { deliveryCostState, orderAmountState, totalOrderAmountState } from "../recoil/cartAmount";
+import { ReactComponent as InfoIcon } from "../assets/info-icon.svg";
+
+export interface ICartAmountProps {}
+
+export default function CartAmount() {
+  const orderAmount = useRecoilValue(orderAmountState);
+  const deliveryCost = useRecoilValue(deliveryCostState);
+  const totalOrderAmount = useRecoilValue(totalOrderAmountState);
+
+  return (
+    <S.CartAmountContainer>
+      <S.CartAmountNoti>
+        <S.InfoIcon />
+        <S.CartAmountNotiText>
+          총 주문 금액이 100,000원 이상일 경우 무료 배송됩니다.
+        </S.CartAmountNotiText>
+      </S.CartAmountNoti>
+
+      <S.UpperCartAmountInfoWrapper>
+        <S.CartAmountInfo>
+          <S.AmountText>주문 금액</S.AmountText>{" "}
+          <S.Amount>{orderAmount.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+        <S.CartAmountInfo>
+          <S.AmountText>배송비</S.AmountText> <S.Amount>{deliveryCost.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+      </S.UpperCartAmountInfoWrapper>
+      <S.LowerCartAmountInfoWrapper>
+        <S.CartAmountInfo>
+          <S.AmountText>총 주문 금액</S.AmountText>{" "}
+          <S.Amount>{totalOrderAmount.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+      </S.LowerCartAmountInfoWrapper>
+    </S.CartAmountContainer>
+  );
+}
+
+const S = {
+  CartAmountContainer: styled.div`
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  `,
+
+  CartAmountNoti: styled.p`
+    font-size: 14px;
+    color: #888;
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+  `,
+
+  UpperCartAmountInfoWrapper: styled.div`
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+  `,
+
+  LowerCartAmountInfoWrapper: styled.div`
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+  `,
+
+  CartAmountInfo: styled.div`
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-top: 12px;
+  `,
+
+  AmountText: styled.span`
+    font-size: 16px;
+    font-weight: 700;
+  `,
+
+  Amount: styled.span`
+    font-size: 24px;
+    font-weight: 700;
+  `,
+  InfoIcon: styled(InfoIcon)`
+    width: 16px;
+    height: 16px;
+  `,
+  CartAmountNotiText: styled.span`
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 15px;
+    color: rgba(10, 13, 19, 1);
+  `,
+};

리뷰: S-dot 컨벤션 좋네요👍 다음에 저도 써봐야겠어요!
@@ -0,0 +1,21 @@
+import { useNavigate } from "react-router-dom";
+import { PATH } from "../constants/path";
+import Button from "./Button";
+import { useRecoilValue } from "recoil";
+import { selectedCartItemIdsState } from "../recoil/selectedCartItemIds";
+
+export default function CartButton() {
+  const navigate = useNavigate();
+  const selectedCartItemIds = useRecoilValue(selectedCartItemIdsState);
+  const isDisabled = selectedCartItemIds.length === 0;
+
+  const handleOrderConfirmButtonClick = () => {
+    navigate(PATH.orderSummary);
+  };
+
+  return (
+    <Button disabled={isDisabled} onClick={handleOrderConfirmButtonClick}>
+      주문 확인
+    </Button>
+  );
+}

리뷰: 버튼 컴포넌트에서 disabled, onClick을 받을 수 있게 해줘도 좋았을 것 같아요! 그럼 `CartButton` 컴포넌트를 별도로 만들지 않고 `Button` 컴포넌트를 바로 재사용할 수 있을 것 같습니다!ㅎㅎ

(아니면 혹시 별도로 이렇게 만들어주신 연유가 있으실까요?)
@@ -0,0 +1,50 @@
+import styled from "styled-components";
+import { cartItemsState } from "../recoil/cartItems";
+import { useRecoilValue } from "recoil";
+import CartTitle from "./CartTitle";
+import CartItemList from "./CartItemList";
+import CartAmount from "./CartAmount";
+import CartButton from "./CartButton";
+
+export default function CartContent() {
+  const cartItems = useRecoilValue(cartItemsState);
+
+  const 내용물 =
+    cartItems.length === 0 ? (
+      <S.EmptyMessage>장바구니에 담은 상품이 없습니다.</S.EmptyMessage>
+    ) : (
+      <>
+        <CartItemList cartItems={cartItems} />
+        <CartAmount />
+      </>
+    );
+
+  return (
+    <>
+      <S.Content>
+        <CartTitle />
+        {내용물}
+      </S.Content>
+      <CartButton />
+    </>
+  );
+}
+
+const S = {
+  EmptyMessage: styled.div`
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 16px;
+    font-weight: 400;
+    line-height: 16px;
+    color: rgba(10, 13, 19, 1);
+    width: fit-content;
+    margin: 0 auto;
+  `,
+  Content: styled.div`
+    padding: 36px 24px 100px 24px;
+    min-height: 100vh;
+  `,
+};

리뷰: 오! 한국어로 변수를 선언해주신 이유가 있으실까요? 보통 변수는 영어로 선언하는데 여기 한국말이라 조금 낮설고 신기하네요!
@@ -0,0 +1,49 @@
+import { useRecoilState, useSetRecoilState } from "recoil";
+import { removeCartItem, updateCartItemQuantity } from "../api/cartItems";
+import { CartItemId } from "../types/cartItems";
+import { rawCartItemsState } from "../recoil/rawCartItems";
+import { selectedCartItemIdsState } from "../recoil/selectedCartItemIds";
+
+export interface UseCartItemsReturn {
+  remove: (cartItemId: CartItemId) => void;
+  updateQuantity: (cartItemId: CartItemId, quantity: number) => void;
+  toggleSelection: (cartItemId: CartItemId) => void;
+}
+
+export const useCartItemControl = (): UseCartItemsReturn => {
+  const [rawCartItems, setRawCartItems] = useRecoilState(rawCartItemsState);
+  const setSelectedCartItemIds = useSetRecoilState(selectedCartItemIdsState);
+
+  const remove = async (cartItemId: CartItemId) => {
+    await removeCartItem(cartItemId);
+
+    setSelectedCartItemIds((prev) => prev.filter((id) => id !== cartItemId));
+
+    const filteredCartItems = rawCartItems.filter((cartItem) => cartItem.id !== cartItemId);
+    setRawCartItems(filteredCartItems);
+  };
+
+  const updateQuantity = async (cartItemId: CartItemId, quantity: number) => {
+    if (quantity < 1) return;
+
+    await updateCartItemQuantity(cartItemId, quantity);
+
+    const updatedCartItems = rawCartItems.map((cartItem) =>
+      cartItem.id === cartItemId ? { ...cartItem, quantity } : cartItem
+    );
+    setRawCartItems(updatedCartItems);
+  };
+
+  const toggleSelection = (cartItemId: CartItemId) => {
+    setSelectedCartItemIds((prev) => {
+      const isSelected = prev.includes(cartItemId);
+      return isSelected ? prev.filter((id) => id !== cartItemId) : [...prev, cartItemId];
+    });
+  };
+
+  return {
+    remove,
+    updateQuantity,
+    toggleSelection,
+  };
+};

리뷰: 별도의 hook으로 분리해주셨군요! 생각해보지 못했던 방법인데, 이렇게 모아두니 굉장히 관심사 분리가 잘 이루어졌다는 생각이 들었어요👍👍👍
@@ -1,10 +1,24 @@
 import "./App.css";
+import { RecoilRoot } from "recoil";
+import { BrowserRouter, Route, Routes } from "react-router-dom";
+
+import CartPage from "./pages/CartPage";
+import { PATH } from "./constants/path";
+import OrderSummaryPage from "./pages/OrderSummaryPage";
+import { ErrorBoundary } from "react-error-boundary";
 
 function App() {
   return (
-    <>
-      <h1>react-shopping-cart</h1>
-    </>
+    <BrowserRouter>
+      <RecoilRoot>
+        <ErrorBoundary fallbackRender={({ error }) => error.message}>
+          <Routes>
+            <Route path={PATH.cart} element={<CartPage />} />
+            <Route path={PATH.orderSummary} element={<OrderSummaryPage />} />
+          </Routes>
+        </ErrorBoundary>
+      </RecoilRoot>
+    </BrowserRouter>
   );
 }
 

리뷰: 제가 유의미한 차이에 대해서는 알아보지 못해서 답변을 섣불리 드릴 수가 없네요..!
다만, 조금 더 최신 문법이다 보니 최신 문법에 맞춰서 적용해봐도 괜찮지 않을까 싶었습니다!
[해당 블로그](https://velog.io/@kimbyeonghwa/createBrowserRouter-vs-BrowserRouter)에서는

모듈화가 가능한 점을 꼽았네요!

저도 모듈화가 가능한 점이 가장 이점이 크다고 생각했어요!
https://github.com/Largopie/react-shopping-cart/blob/step1/src/router.tsx

개인적으로는 이와 같이 모듈화해서 사용할 수 있어서 코드를 보다 직관적으로 파악할 수 있었던 것 같습니다!

++ 공식문서에서도 권장하는 방법이네요!

> This is the recommended router for all React Router web projects. It uses the [DOM History API](https://developer.mozilla.org/en-US/docs/Web/API/History) to update the URL and manage the history stack.
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+
+export interface IButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
+  disabled?: boolean;
+}
+
+export default function Button({ children, ...attributes }: IButtonProps) {
+  return <S.Button {...attributes}>{children}</S.Button>;
+}
+
+const S = {
+  Button: styled.button`
+    position: fixed;
+    bottom: 0;
+    max-width: 429px;
+
+    height: 64px;
+    font-size: 16px;
+    font-weight: 700;
+    text-align: center;
+    width: 100%;
+
+    background-color: black;
+    color: white;
+    border: none;
+    cursor: pointer;
+    &:disabled {
+      background-color: rgba(190, 190, 190, 1);
+    }
+  `,
+};

리뷰: 저도 공용적으로 사용하는 컴포넌트는 보통 `common` 사용합니다! 🙌

단지 스타일을 위해서 사용하는 코드라면 styled라고 붙여도 괜찮을 것 같네요!
@@ -0,0 +1,35 @@
+package christmas.domain.discount;
+
+import christmas.domain.discount.dto.DiscountResponseDto;
+import christmas.domain.order.OrderRequestDto;
+
+import java.time.LocalDate;
+
+public class ChristmasDiscounter implements Discounter{
+
+    private final LocalDate START_DATE = LocalDate.of(2023,12,1);
+    private final LocalDate END_DATE = LocalDate.of(2023,12,25);
+
+    private final String DISCOUNT_NAME = "크리스마스 디데이 할인";
+
+    @Override
+    public DiscountResponseDto discount(OrderRequestDto orderRequest) {
+
+        LocalDate orderDate = orderRequest.getOrderDate();
+        int totalDiscount = 0;
+
+        if (isValidDateRange(orderDate)) {
+            totalDiscount = calculateDiscountPrice(orderDate);
+        }
+
+        return new DiscountResponseDto(totalDiscount, DISCOUNT_NAME);
+    }
+
+    private boolean isValidDateRange(LocalDate orderDate) {
+        return !orderDate.isAfter(END_DATE) && !orderDate.isBefore(START_DATE);
+    }
+
+    private int calculateDiscountPrice(LocalDate orderDate) {
+        return (orderDate.getDayOfMonth() -1 ) * 100 + 1000;
+    }
+}

리뷰: 100과 1000도 상수화하는건 어떨까요??!
@@ -0,0 +1,45 @@
+package christmas.domain.discount;
+
+import christmas.domain.discount.dto.DiscountResponseDto;
+import christmas.domain.discount.dto.FreeBieResponseDto;
+import christmas.domain.menu.DrinkMenu;
+import christmas.domain.menu.Menu;
+import christmas.domain.order.OrderRequestDto;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+public class FreeBieDiscounter implements Discounter {
+
+    private final String DISCOUNT_NAME = "증정 할인";
+    private final Menu FREEBIE_MENU = DrinkMenu.CHAMPAGNE;
+    private final int FREEBIE_MIN_PRICE = 120_000;
+
+    @Override
+    public DiscountResponseDto discount(OrderRequestDto orderRequest) {
+
+        List<Menu> menus = orderRequest.getMenus();
+        LocalDate orderDate = orderRequest.getOrderDate();
+
+        if (isFreeBie(menus, orderDate)) {
+            return new FreeBieResponseDto(Optional.of(FREEBIE_MENU), DISCOUNT_NAME);
+        }
+
+        return new FreeBieResponseDto(Optional.empty(), DISCOUNT_NAME);
+    }
+
+    private boolean isFreeBie(List<Menu> menus, LocalDate orderDate) {
+        int totalPrice = 0;
+
+        if (orderDate.getMonth().getValue() != 12 || orderDate.getYear() != 2023) {
+            return false;
+        }
+
+        for (Menu menu : menus) {
+            totalPrice += menu.getPrice();
+        }
+
+        return totalPrice >= FREEBIE_MIN_PRICE;
+    }
+}

리뷰: 2023년 12월에만 적용되는 이벤트라는게 눈에 띄는 것 같아요! 저는 생각못한 부분인데 세세하게 잘 하신 것 같네요!!
@@ -0,0 +1,33 @@
+package christmas.domain.discount.dto;
+
+import christmas.domain.menu.Menu;
+
+import java.util.Optional;
+
+public class FreeBieResponseDto extends DiscountResponseDto{
+
+    private final Optional<Menu> freeBieMenu;
+
+    public FreeBieResponseDto(Optional<Menu> freeBieMenu, String discountName) {
+        super(freeBieMenu.orElse(new FreeMenu()).getPrice(), discountName);
+        this.freeBieMenu = freeBieMenu;
+    }
+
+    public Optional<Menu> getFreeBieMenu() {
+        return freeBieMenu;
+    }
+
+    static private class FreeMenu implements Menu {
+        
+
+        @Override
+        public int getPrice() {
+            return 0;
+        }
+
+        @Override
+        public String getName() {
+            return null;
+        }
+    }
+}

리뷰: 오.. 이런 형태는 처음보네요!
@@ -0,0 +1,84 @@
+package christmas.domain.order;
+
+import christmas.domain.discount.Discounter;
+import christmas.domain.discount.dto.DiscountResponseDto;
+import christmas.domain.discount.dto.FreeBieResponseDto;
+import christmas.domain.menu.DrinkMenu;
+import christmas.domain.menu.Menu;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class OrderImpl implements Order{
+
+    private final List<Discounter> discounters;
+
+    public OrderImpl(List<Discounter> discounters) {
+        this.discounters = discounters;
+    }
+
+    @Override
+    public OrderResponseDto order(OrderRequestDto orderRequestDto) {
+        assertValidOrder(orderRequestDto.getMenus());
+
+        List<DiscountResponseDto> discountResult = getDiscountResponses(orderRequestDto);
+        int discountPrice = getDiscountPrice(discountResult);
+        int priceBeforeDiscount = getPriceBeforeDiscount(orderRequestDto.getMenus());
+        int priceAfterDiscount = priceBeforeDiscount - discountPrice;
+
+        OrderResponseDto result = new OrderResponseDto();
+        result.setDiscountResults(discountResult);
+        result.setDiscountPrice(discountPrice);
+        result.setPriceAfterDiscount(priceAfterDiscount);
+        result.setPriceBeforeDiscount(priceBeforeDiscount);
+
+
+        return result;
+    }
+
+    private void assertValidOrder(List<Menu> menus) {
+        boolean validCheck = false;
+
+        for (Menu menu : menus) {
+            if (!(menu instanceof DrinkMenu)) {
+                validCheck = true;
+                break;
+            }
+        }
+
+        if (!validCheck) {
+            throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+        }
+    }
+
+
+    private List<DiscountResponseDto> getDiscountResponses(OrderRequestDto orderRequestDto){
+        List<DiscountResponseDto> result = new ArrayList<>();
+
+        for (Discounter discounter : discounters) {
+            result.add(discounter.discount(orderRequestDto));
+        }
+        return result;
+    }
+
+
+    //증정은 따로 처리 필요.
+    private int getDiscountPrice(List<DiscountResponseDto> discounts) {
+        int discountPrice = 0;
+        for (DiscountResponseDto discount : discounts) {
+            discountPrice += discount.getTotalDiscount();
+        }
+
+        return discountPrice;
+    }
+
+    private int getPriceBeforeDiscount(List<Menu> menus) {
+        int priceBeforeDiscount = 0;
+
+        for (Menu menu : menus) {
+            priceBeforeDiscount += menu.getPrice();
+        }
+
+        return priceBeforeDiscount;
+    }
+}

리뷰: 메서드가 음료로만 이루어졌는지 테스트하는 것 같아요! 메서드명을 모두 음료인지 체크하는 메서드라는 걸 알 수 있게 하는건 어떨까요?
ValidOrder는 전체적인 유효성에 대해서 생각하게될 것 같아요!
@@ -0,0 +1,43 @@
+package christmas.domain.user;
+
+import java.util.Optional;
+
+public enum Badge {
+    STAR(5_000, "별"),
+    TREE(10_000, "트리"),
+    SANTA(20_000, "산타");
+
+    private final int minPrice;
+    private final String message;
+
+    Badge(int minPrice, String message) {
+        this.minPrice = minPrice;
+        this.message = message;
+    }
+
+    public int getMinPrice() {
+        return minPrice;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public static Optional<Badge> findByPrice(int discountPrice) {
+        Badge result = null;
+
+        for (Badge badge : Badge.values()) {
+            if (result == null && badge.getMinPrice() <= discountPrice) {
+                result = badge;
+            }
+            else if (badge.getMinPrice() <= discountPrice && badge.getMinPrice() >= result.getMinPrice() ) {
+                result = badge;
+            }
+        }
+
+        if (result == null) {
+            return Optional.empty();
+        }
+        return Optional.of(result);
+    }
+}

리뷰: message 보다는 badgeType 같은 필드명이 더 나을 것 같아요!
@@ -0,0 +1,28 @@
+package christmas.domain.user;
+
+import christmas.domain.order.Order;
+import christmas.domain.order.OrderRequestDto;
+import christmas.domain.order.OrderResponseDto;
+
+import java.util.Optional;
+
+public class User {
+
+    private Optional<Badge> badge;
+    private final Order order;
+
+    public User(Order order) {
+        this.order = order;
+    }
+
+    public OrderResponseDto order(OrderRequestDto request) {
+        OrderResponseDto response = order.order(request);
+        this.badge = Badge.findByPrice(response.getDiscountPrice());
+
+        return response;
+    }
+
+    public Optional<Badge> getBadge() {
+        return badge;
+    }
+}

리뷰: 위에서도 나왔지만 Optional은 저는 처음 보는 것 같아요! 자바가 아직 서툴러서..ㅎ
덕분에 새로운 부분에 대해 공부하고 갈 수 있겠네요!!
@@ -0,0 +1,81 @@
+package christmas.presentation;
+
+import camp.nextstep.edu.missionutils.Console;
+import christmas.domain.menu.Menu;
+import christmas.domain.order.OrderErrorMessage;
+
+import java.time.LocalDate;
+import java.util.*;
+
+public class InputView {
+
+    public LocalDate inputDate() throws IllegalArgumentException {
+
+        try {
+            System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
+            int date = Integer.parseInt(Console.readLine());
+
+            if (date < 1 || date > 31) {
+                throw new IllegalArgumentException(ViewErrorMessage.INVALID_DATE_FORMAT.getMessage());
+            }
+
+            return LocalDate.of(2023,12,date);
+        } catch (NumberFormatException ex) {
+            throw new IllegalArgumentException(ViewErrorMessage.INVALID_DATE_FORMAT.getMessage());
+        }
+    }
+
+    public Map<String, Integer> inputMenus() throws IllegalArgumentException {
+        System.out.println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)");
+        String userInput = Console.readLine();
+        try {
+            Map<String, Integer> result = parsingMenus(userInput);
+            if (result.keySet().size() == 0) {
+                throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+            }
+
+            return result;
+        } catch (IllegalArgumentException ex) {
+            throw ex;
+        }
+    }
+
+
+
+    private Map<String, Integer> parsingMenus(String userOrder) throws IllegalArgumentException {
+        Map<String,Integer> menus = new HashMap<>();
+        Set<String> orderMenu = new HashSet<>();
+
+        List<String> splitMenus = List.of(userOrder.split(","));
+        for (String splitMenu : splitMenus) {
+            Map<String, Integer> tmpResult = parsingMenu(splitMenu);
+            String tmpMenuName = tmpResult.keySet().iterator().next();
+
+            if (orderMenu.contains(tmpMenuName)) {
+                throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+            }
+            orderMenu.add(tmpMenuName);
+            menus.put(tmpMenuName, tmpResult.get(tmpMenuName));
+        }
+        return menus;
+    }
+
+    private Map<String, Integer> parsingMenu(String menu) {
+        Map<String, Integer> parsingResult = new HashMap<>();
+        int i = menu.lastIndexOf("-");
+
+        if (i == -1) {
+            throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+        }
+
+        try {
+            parsingResult.put(menu.substring(0,i), Integer.parseInt(menu.substring(i + 1)));
+        } catch (IllegalArgumentException ex) {
+            throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+        } catch (Exception ex) {
+            throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+        }
+
+        return parsingResult;
+    }
+}

리뷰: 저는 view에서는 숫자가 들어왔는지만 체크하고 1~31일에 해당하는지는 도메인 객체에서 체크했어요!
저는 이 부분에 대해서 어디에서 구현할지 고민했는데 view에서 체크하신 이유가 있을까요?
@@ -0,0 +1,81 @@
+package christmas.presentation;
+
+import camp.nextstep.edu.missionutils.Console;
+import christmas.domain.menu.Menu;
+import christmas.domain.order.OrderErrorMessage;
+
+import java.time.LocalDate;
+import java.util.*;
+
+public class InputView {
+
+    public LocalDate inputDate() throws IllegalArgumentException {
+
+        try {
+            System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
+            int date = Integer.parseInt(Console.readLine());
+
+            if (date < 1 || date > 31) {
+                throw new IllegalArgumentException(ViewErrorMessage.INVALID_DATE_FORMAT.getMessage());
+            }
+
+            return LocalDate.of(2023,12,date);
+        } catch (NumberFormatException ex) {
+            throw new IllegalArgumentException(ViewErrorMessage.INVALID_DATE_FORMAT.getMessage());
+        }
+    }
+
+    public Map<String, Integer> inputMenus() throws IllegalArgumentException {
+        System.out.println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)");
+        String userInput = Console.readLine();
+        try {
+            Map<String, Integer> result = parsingMenus(userInput);
+            if (result.keySet().size() == 0) {
+                throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+            }
+
+            return result;
+        } catch (IllegalArgumentException ex) {
+            throw ex;
+        }
+    }
+
+
+
+    private Map<String, Integer> parsingMenus(String userOrder) throws IllegalArgumentException {
+        Map<String,Integer> menus = new HashMap<>();
+        Set<String> orderMenu = new HashSet<>();
+
+        List<String> splitMenus = List.of(userOrder.split(","));
+        for (String splitMenu : splitMenus) {
+            Map<String, Integer> tmpResult = parsingMenu(splitMenu);
+            String tmpMenuName = tmpResult.keySet().iterator().next();
+
+            if (orderMenu.contains(tmpMenuName)) {
+                throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+            }
+            orderMenu.add(tmpMenuName);
+            menus.put(tmpMenuName, tmpResult.get(tmpMenuName));
+        }
+        return menus;
+    }
+
+    private Map<String, Integer> parsingMenu(String menu) {
+        Map<String, Integer> parsingResult = new HashMap<>();
+        int i = menu.lastIndexOf("-");
+
+        if (i == -1) {
+            throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+        }
+
+        try {
+            parsingResult.put(menu.substring(0,i), Integer.parseInt(menu.substring(i + 1)));
+        } catch (IllegalArgumentException ex) {
+            throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+        } catch (Exception ex) {
+            throw new IllegalArgumentException(OrderErrorMessage.INVALID_ORDER.getMessage());
+        }
+
+        return parsingResult;
+    }
+}

리뷰: 위에서랑 같은 맥락인데 View에서는 입력을 받고, utils 같은 부분에서 입력값 형태를 바꾸어주는 역할을 해야한다고 저는 생각했어요! Inputview 내에서 하신 이유가 궁금해요!
@@ -0,0 +1,35 @@
+package christmas.domain.discount;
+
+import christmas.domain.discount.dto.DiscountResponseDto;
+import christmas.domain.order.OrderRequestDto;
+
+import java.time.LocalDate;
+
+public class ChristmasDiscounter implements Discounter{
+
+    private final LocalDate START_DATE = LocalDate.of(2023,12,1);
+    private final LocalDate END_DATE = LocalDate.of(2023,12,25);
+
+    private final String DISCOUNT_NAME = "크리스마스 디데이 할인";
+
+    @Override
+    public DiscountResponseDto discount(OrderRequestDto orderRequest) {
+
+        LocalDate orderDate = orderRequest.getOrderDate();
+        int totalDiscount = 0;
+
+        if (isValidDateRange(orderDate)) {
+            totalDiscount = calculateDiscountPrice(orderDate);
+        }
+
+        return new DiscountResponseDto(totalDiscount, DISCOUNT_NAME);
+    }
+
+    private boolean isValidDateRange(LocalDate orderDate) {
+        return !orderDate.isAfter(END_DATE) && !orderDate.isBefore(START_DATE);
+    }
+
+    private int calculateDiscountPrice(LocalDate orderDate) {
+        return (orderDate.getDayOfMonth() -1 ) * 100 + 1000;
+    }
+}

리뷰: 상수화에 관해서 크게 생각하지 않았었네요. 상수화하는 것이 좋을 것 같습니다!