ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,59 @@
+import { useState } from 'react';
+import { useModalContext } from '../../../contexts/ModalContext';
+import Modal from '../../_common/Modal/Modal';
+import * as S from '../Movie.styled';
+import MovieDetail from '../MovieDetail/MovieDetail';
+import MovieItem from '../MovieItem/MovieItem';
+import FetchTargetBox from '../../_common/FetchTargetBox/FetchTargetBox';
+import SkeletonColumn from '../../_common/Skeleton/SkeletonColumn';
+import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
+import useMovieQuery from '../../../hooks/useMovieQuery';
+
+interface MovieListProps {
+  keyword: string;
+}
+
+function MovieList({ keyword }: MovieListProps) {
+  const [selectedMovie, setSelectedMovie] = useState<number>(0);
+
+  const { isOpenModal, openModal, closeModal } = useModalContext();
+
+  const handleModalOpen = (movieId: number) => {
+    setSelectedMovie(movieId);
+    openModal('movie');
+  };
+
+  const {
+    movieList,
+    fetchNextPage,
+    isFetchingNextPage,
+    hasNextPage,
+    isLoading,
+  } = useMovieQuery({ keyword });
+
+  const { nextFetchTargetRef } = useInfiniteScroll({
+    fetchNextPage,
+    hasNextPage,
+    movieList,
+  });
+
+  return (
+    <S.MovieListLayout>
+      <S.ItemList>
+        {isLoading && <SkeletonColumn count={20} />}
+        {movieList.map(movie => (
+          <MovieItem movie={movie} key={movie.id} onClick={handleModalOpen} />
+        ))}
+      </S.ItemList>
+      {!isFetchingNextPage && hasNextPage && (
+        <FetchTargetBox ref={nextFetchTargetRef} />
+      )}
+
+      <Modal onClose={() => closeModal('movie')} isOpen={isOpenModal}>
+        <MovieDetail movieId={selectedMovie} />
+      </Modal>
+    </S.MovieListLayout>
+  );
+}
+
+export default MovieList;

리뷰: 옵저버 관련 동작을 별도의 훅으로 분리하면 코드가 더 간결해질 것 같아요!
@@ -0,0 +1,59 @@
+import { useState } from 'react';
+import { useModalContext } from '../../../contexts/ModalContext';
+import Modal from '../../_common/Modal/Modal';
+import * as S from '../Movie.styled';
+import MovieDetail from '../MovieDetail/MovieDetail';
+import MovieItem from '../MovieItem/MovieItem';
+import FetchTargetBox from '../../_common/FetchTargetBox/FetchTargetBox';
+import SkeletonColumn from '../../_common/Skeleton/SkeletonColumn';
+import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
+import useMovieQuery from '../../../hooks/useMovieQuery';
+
+interface MovieListProps {
+  keyword: string;
+}
+
+function MovieList({ keyword }: MovieListProps) {
+  const [selectedMovie, setSelectedMovie] = useState<number>(0);
+
+  const { isOpenModal, openModal, closeModal } = useModalContext();
+
+  const handleModalOpen = (movieId: number) => {
+    setSelectedMovie(movieId);
+    openModal('movie');
+  };
+
+  const {
+    movieList,
+    fetchNextPage,
+    isFetchingNextPage,
+    hasNextPage,
+    isLoading,
+  } = useMovieQuery({ keyword });
+
+  const { nextFetchTargetRef } = useInfiniteScroll({
+    fetchNextPage,
+    hasNextPage,
+    movieList,
+  });
+
+  return (
+    <S.MovieListLayout>
+      <S.ItemList>
+        {isLoading && <SkeletonColumn count={20} />}
+        {movieList.map(movie => (
+          <MovieItem movie={movie} key={movie.id} onClick={handleModalOpen} />
+        ))}
+      </S.ItemList>
+      {!isFetchingNextPage && hasNextPage && (
+        <FetchTargetBox ref={nextFetchTargetRef} />
+      )}
+
+      <Modal onClose={() => closeModal('movie')} isOpen={isOpenModal}>
+        <MovieDetail movieId={selectedMovie} />
+      </Modal>
+    </S.MovieListLayout>
+  );
+}
+
+export default MovieList;

리뷰: 오옹 이 방식 깔끔하고 좋네요 👍👍 리팩토링하러 가야겠... 😂
@@ -0,0 +1,29 @@
+import { Movie } from '../../../types/movie.type';
+import * as S from '../Movie.styled';
+
+interface MovieItemProps {
+  movie: Movie;
+  onClick: (movieId: number) => void;
+}
+
+function MovieItem({ movie, onClick }: MovieItemProps) {
+  return (
+    <li key={movie.id} onClick={() => onClick(movie.id)}>
+      <S.ItemCard>
+        <S.ItemThumbnail
+          src={`https://image.tmdb.org/t/p/w220_and_h330_face${movie.poster_path}`}
+          loading="lazy"
+          alt={movie.title}
+        />
+
+        <S.ItemTitle>{movie.title}</S.ItemTitle>
+        <S.ItemScoreBox>
+          <S.ItemScore>{movie.vote_average.toFixed(1)} </S.ItemScore>
+          <S.StarImage src="./star_filled.png" alt="별점" />
+        </S.ItemScoreBox>
+      </S.ItemCard>
+    </li>
+  );
+}
+
+export default MovieItem;

리뷰: 여기서도 소수점 보정을 해야 할 것 같아유
@@ -0,0 +1,20 @@
+import { DISPLAY_SIZE } from '../../../constants/displaySize';
+import LargeHeader from './LargeHeader';
+import MobileHeader from './MobileHeader';
+
+interface HeaderProps {
+  searchKeyword: string;
+  onSearch: (keyword: string) => void;
+}
+
+function Header({ searchKeyword, onSearch }: HeaderProps) {
+  const isMobile = document.documentElement.clientWidth <= DISPLAY_SIZE.mobile;
+
+  return isMobile ? (
+    <MobileHeader searchKeyword={searchKeyword} onSearch={onSearch} />
+  ) : (
+    <LargeHeader searchKeyword={searchKeyword} onSearch={onSearch} />
+  );
+}
+
+export default Header;

리뷰: 오 Header에서는 사용되지 않는데 들어가있군요! Header는 단순히 레이아웃 기능만 하고 있어서 타입을 확장하지 않아도 될 것 같아요 짚어주셔서 감사해요~!

> common 폴더에만 _가 붙어있는데 처음 보는 컨벤션이라 어떤 의미인지도 궁금해요 ㅎㅎ

_는 폴더 최상단에 띄워지도록 하기 위해 넣은 컨벤션이랍니다!🤗
@@ -0,0 +1,58 @@
+import React, { useReducer } from 'react';
+import * as S from './Header.styled';
+
+interface HeaderProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  searchKeyword: string;
+  onSearch: (keyword: string) => void;
+}
+
+type State = boolean;
+type Action = { type: 'TOGGLE' } | { type: 'OPEN' } | { type: 'CLOSE' };
+
+function reducer(state: State, action: Action): State {
+  switch (action.type) {
+    case 'TOGGLE':
+      return !state;
+    case 'OPEN':
+      return true;
+    case 'CLOSE':
+      return false;
+    default:
+      return state;
+  }
+}
+
+function MobileHeader({ searchKeyword, onSearch, ...rest }: HeaderProps) {
+  const [isExpanded, updateState] = useReducer(reducer, false);
+
+  const handleSearchIconClick = () => {
+    updateState({ type: 'TOGGLE' });
+  };
+
+  return (
+    <S.Layout>
+      <a href="https://chosim-dvlpr.github.io/react-movie-review/">
+        <S.LogoButton>
+          <S.LogoImage src="./logo.png" />
+        </S.LogoButton>
+      </a>
+      <S.InputBox $isExpanded={isExpanded}>
+        <S.Input
+          {...rest}
+          value={searchKeyword}
+          onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
+            onSearch(event.target.value)
+          }
+          $isDisabled={!isExpanded}
+          disabled={!isExpanded}
+        />
+        <S.SearchIcon
+          src="./search_button.png"
+          onClick={handleSearchIconClick}
+        />
+      </S.InputBox>
+    </S.Layout>
+  );
+}
+
+export default MobileHeader;

리뷰: 맞아요! 그리고 input이 닫혀 있는 경우 input을 클릭했을 때 값이 입력되면서 돋보기 버튼 위로 보여지는 문제가 발생해서 disabled 속성을 추가했습니당
@@ -0,0 +1,58 @@
+import React, { useReducer } from 'react';
+import * as S from './Header.styled';
+
+interface HeaderProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  searchKeyword: string;
+  onSearch: (keyword: string) => void;
+}
+
+type State = boolean;
+type Action = { type: 'TOGGLE' } | { type: 'OPEN' } | { type: 'CLOSE' };
+
+function reducer(state: State, action: Action): State {
+  switch (action.type) {
+    case 'TOGGLE':
+      return !state;
+    case 'OPEN':
+      return true;
+    case 'CLOSE':
+      return false;
+    default:
+      return state;
+  }
+}
+
+function MobileHeader({ searchKeyword, onSearch, ...rest }: HeaderProps) {
+  const [isExpanded, updateState] = useReducer(reducer, false);
+
+  const handleSearchIconClick = () => {
+    updateState({ type: 'TOGGLE' });
+  };
+
+  return (
+    <S.Layout>
+      <a href="https://chosim-dvlpr.github.io/react-movie-review/">
+        <S.LogoButton>
+          <S.LogoImage src="./logo.png" />
+        </S.LogoButton>
+      </a>
+      <S.InputBox $isExpanded={isExpanded}>
+        <S.Input
+          {...rest}
+          value={searchKeyword}
+          onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
+            onSearch(event.target.value)
+          }
+          $isDisabled={!isExpanded}
+          disabled={!isExpanded}
+        />
+        <S.SearchIcon
+          src="./search_button.png"
+          onClick={handleSearchIconClick}
+        />
+      </S.InputBox>
+    </S.Layout>
+  );
+}
+
+export default MobileHeader;

리뷰: 처음에는 확장성을 염두하고 객체로 만들었는데 이 경우에는 `isExpanded`의 boolean 상태 변경만을 목적으로 하니 객체로 만들지 않고 올리가 제안한 방식도 괜찮을 것 같다는 생각이 드네요~!
@@ -0,0 +1,58 @@
+import React, { useReducer } from 'react';
+import * as S from './Header.styled';
+
+interface HeaderProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  searchKeyword: string;
+  onSearch: (keyword: string) => void;
+}
+
+type State = boolean;
+type Action = { type: 'TOGGLE' } | { type: 'OPEN' } | { type: 'CLOSE' };
+
+function reducer(state: State, action: Action): State {
+  switch (action.type) {
+    case 'TOGGLE':
+      return !state;
+    case 'OPEN':
+      return true;
+    case 'CLOSE':
+      return false;
+    default:
+      return state;
+  }
+}
+
+function MobileHeader({ searchKeyword, onSearch, ...rest }: HeaderProps) {
+  const [isExpanded, updateState] = useReducer(reducer, false);
+
+  const handleSearchIconClick = () => {
+    updateState({ type: 'TOGGLE' });
+  };
+
+  return (
+    <S.Layout>
+      <a href="https://chosim-dvlpr.github.io/react-movie-review/">
+        <S.LogoButton>
+          <S.LogoImage src="./logo.png" />
+        </S.LogoButton>
+      </a>
+      <S.InputBox $isExpanded={isExpanded}>
+        <S.Input
+          {...rest}
+          value={searchKeyword}
+          onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
+            onSearch(event.target.value)
+          }
+          $isDisabled={!isExpanded}
+          disabled={!isExpanded}
+        />
+        <S.SearchIcon
+          src="./search_button.png"
+          onClick={handleSearchIconClick}
+        />
+      </S.InputBox>
+    </S.Layout>
+  );
+}
+
+export default MobileHeader;

리뷰: 오 정말 토글만 사용 중이군요,,, open과 close는 습관적으로 작성했던 것 같네요 ㅋㅋㅋ주의해야겠어요😂 `useReducer`를 사용한 이유는 `isExpanded` state를 업데이트 하는 방식을 명시적으로 나타내기 위함이었어요! `useState`는 상태를 어떤 값으로 변경하는지 나타낸다면, `useReducer`는 어떤 방식으로 변경할지를 보여줄 수 있다고 생각해서 `useReducer`를 사용했습니닷
@@ -0,0 +1,49 @@
+package christmas.domain;
+
+import christmas.error.ErrorCode;
+import java.util.Arrays;
+
+public enum MenuItem {
+
+    양송이스프(6_000, "양송이스프", MenuCategory.Appetizers),
+    타파스(5_500, "타파스", MenuCategory.Appetizers),
+    시저샐러드(8_000, "시저샐러드", MenuCategory.Appetizers),
+    티본스테이크(55_000, "티본스테이크", MenuCategory.MainDishes),
+    바비큐립(54_000, "바비큐립", MenuCategory.MainDishes),
+    해산물파스타(35_000, "해산물파스타", MenuCategory.MainDishes),
+    크리스마스파스타(25_000, "크리스마스파스타", MenuCategory.MainDishes),
+    초코케이크(15_000, "초코케이크", MenuCategory.Desserts),
+    아이스크림(5_000, "아이스크림", MenuCategory.Desserts),
+    제로콜라(3_000, "제로콜라", MenuCategory.Drinks),
+    레드와인(60_000, "레드와인", MenuCategory.Drinks),
+    샴페인(25_000, "샴페인", MenuCategory.Drinks);
+
+    private final int price;
+    private final String name;
+    private final MenuCategory category;
+
+    MenuItem(int price, String name, MenuCategory category) {
+        this.price = price;
+        this.name = name;
+        this.category = category;
+    }
+
+    public static MenuItem getMenuList(String menuName) {
+        return Arrays.stream(MenuItem.values())
+                .filter(menuItem -> menuItem.name.equals(menuName))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException(ErrorCode.INVALID_MENU_FORMAT.getMessage()));
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public MenuCategory getCategory() {
+        return category;
+    }
+}

리뷰: MenuItem을 enum으로 만드는 방식이 좋아보여요!
@@ -0,0 +1,69 @@
+package christmas.domain.benefit;
+
+import christmas.domain.Calender;
+import christmas.domain.Order;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+public enum BenefitCategory {
+
+    평일할인("평일할인", 2_023, input -> {
+
+        LocalDate date = (LocalDate) input;
+        if (date.getDayOfWeek().getValue() == 7) {
+            return true;
+        }
+        return date.getDayOfWeek().getValue() <= 4 && date.getDayOfWeek().getValue() >= 1;
+    }),
+    주말할인("주말할인", 2_023, input -> {
+
+        LocalDate date = (LocalDate) input;
+        return date.getDayOfWeek().getValue() <= 6 && date.getDayOfWeek().getValue() >= 5;
+    }),
+    특별할인("특별할인", 1_000, input -> {
+
+        LocalDate date = (LocalDate) input;
+        return date.getDayOfWeek().getValue() == 7 || date.getDayOfMonth() == 25;
+    }),
+    증정이벤트("증정이벤트", 25_000, input -> false),
+    크리스마스_디데이할인("크리스마스_디데이할인", 100, input -> {
+        LocalDate date = (LocalDate) input;
+        if (date.getDayOfMonth() <= 25) {
+            return true;
+        }
+        return false;
+    }),
+    ;
+
+    private final String name;
+    private final int price;
+    private final Function<Object, Boolean> dateValidator;
+
+    BenefitCategory(String name, int price, Function<Object, Boolean> dateValidator) {
+        this.name = name;
+        this.price = price;
+        this.dateValidator = dateValidator;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public static List<BenefitCategory> getAvailableBenefitItems(Calender calender, Order order) {
+        List<BenefitCategory> benefitItems = new java.util.ArrayList<>(Arrays.stream(BenefitCategory.values())
+                .filter(benefitItem -> benefitItem.dateValidator.apply(calender.getDate()))
+                .toList());
+
+        if (order.calculateTotalPrice() >= 120_000) {
+            benefitItems.add(증정이벤트);
+        }
+
+        return benefitItems;
+    }
+}

리뷰: enum 클래스 내부에 로직이 많으면 너무 복잡하지는 않을까요?
@@ -0,0 +1,69 @@
+package christmas.domain.benefit;
+
+import christmas.domain.Calender;
+import christmas.domain.Order;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+public enum BenefitCategory {
+
+    평일할인("평일할인", 2_023, input -> {
+
+        LocalDate date = (LocalDate) input;
+        if (date.getDayOfWeek().getValue() == 7) {
+            return true;
+        }
+        return date.getDayOfWeek().getValue() <= 4 && date.getDayOfWeek().getValue() >= 1;
+    }),
+    주말할인("주말할인", 2_023, input -> {
+
+        LocalDate date = (LocalDate) input;
+        return date.getDayOfWeek().getValue() <= 6 && date.getDayOfWeek().getValue() >= 5;
+    }),
+    특별할인("특별할인", 1_000, input -> {
+
+        LocalDate date = (LocalDate) input;
+        return date.getDayOfWeek().getValue() == 7 || date.getDayOfMonth() == 25;
+    }),
+    증정이벤트("증정이벤트", 25_000, input -> false),
+    크리스마스_디데이할인("크리스마스_디데이할인", 100, input -> {
+        LocalDate date = (LocalDate) input;
+        if (date.getDayOfMonth() <= 25) {
+            return true;
+        }
+        return false;
+    }),
+    ;
+
+    private final String name;
+    private final int price;
+    private final Function<Object, Boolean> dateValidator;
+
+    BenefitCategory(String name, int price, Function<Object, Boolean> dateValidator) {
+        this.name = name;
+        this.price = price;
+        this.dateValidator = dateValidator;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public static List<BenefitCategory> getAvailableBenefitItems(Calender calender, Order order) {
+        List<BenefitCategory> benefitItems = new java.util.ArrayList<>(Arrays.stream(BenefitCategory.values())
+                .filter(benefitItem -> benefitItem.dateValidator.apply(calender.getDate()))
+                .toList());
+
+        if (order.calculateTotalPrice() >= 120_000) {
+            benefitItems.add(증정이벤트);
+        }
+
+        return benefitItems;
+    }
+}

리뷰: 증정 이벤트는 날짜와는 관련이 없는 이벤트라 function이 따로 사용되지 않네요.
증정 이벤트도 이 클래스에서 처리하는게 좋은가요?
@@ -0,0 +1,40 @@
+package christmas.domain.benefit;
+
+import christmas.domain.Calender;
+import christmas.domain.Order;
+import christmas.domain.benefit.benefitItems.BenefitItem;
+import christmas.domain.benefit.benefitItems.ChristmasBenefit;
+import christmas.domain.benefit.benefitItems.GiftBenefit;
+import christmas.domain.benefit.benefitItems.SpecialBenefit;
+import christmas.domain.benefit.benefitItems.WeekdaysBenefit;
+import christmas.domain.benefit.benefitItems.WeekendBenefit;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BenefitItemFactory {
+
+    public static List<BenefitItem> createBenefitItems(List<BenefitCategory> benefitCategory, Calender calender, Order order) {
+        List<BenefitItem> benefitItems = new ArrayList<>();
+        for(BenefitCategory category : benefitCategory) {
+            benefitItems.add(createBenefitItem(category, calender, order));
+        }
+        return benefitItems;
+    }
+
+    private static BenefitItem createBenefitItem(BenefitCategory category, Calender calender, Order order) {
+        switch(category) {
+            case 평일할인:
+                return WeekdaysBenefit.of(category, calender, order);
+            case 주말할인:
+                return WeekendBenefit.of(category, calender, order);
+            case 크리스마스_디데이할인:
+                return ChristmasBenefit.of(category, calender, order);
+            case 특별할인:
+                return SpecialBenefit.of(category, calender, order);
+            case 증정이벤트:
+                return GiftBenefit.of(category, calender, order);
+            default:
+                throw new IllegalArgumentException("잘못된 혜택 카테고리입니다.");
+        }
+    }
+}

리뷰: 팩토리 패턴을 사용하신 것 같은데 여기서 팩토리 패턴을 사용하면 어떤 이점을 얻을 수 있는지 궁금합니다!
@@ -0,0 +1,40 @@
+package christmas.domain.benefit;
+
+import christmas.domain.Calender;
+import christmas.domain.Order;
+import christmas.domain.benefit.benefitItems.BenefitItem;
+import christmas.domain.benefit.benefitItems.ChristmasBenefit;
+import christmas.domain.benefit.benefitItems.GiftBenefit;
+import christmas.domain.benefit.benefitItems.SpecialBenefit;
+import christmas.domain.benefit.benefitItems.WeekdaysBenefit;
+import christmas.domain.benefit.benefitItems.WeekendBenefit;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BenefitItemFactory {
+
+    public static List<BenefitItem> createBenefitItems(List<BenefitCategory> benefitCategory, Calender calender, Order order) {
+        List<BenefitItem> benefitItems = new ArrayList<>();
+        for(BenefitCategory category : benefitCategory) {
+            benefitItems.add(createBenefitItem(category, calender, order));
+        }
+        return benefitItems;
+    }
+
+    private static BenefitItem createBenefitItem(BenefitCategory category, Calender calender, Order order) {
+        switch(category) {
+            case 평일할인:
+                return WeekdaysBenefit.of(category, calender, order);
+            case 주말할인:
+                return WeekendBenefit.of(category, calender, order);
+            case 크리스마스_디데이할인:
+                return ChristmasBenefit.of(category, calender, order);
+            case 특별할인:
+                return SpecialBenefit.of(category, calender, order);
+            case 증정이벤트:
+                return GiftBenefit.of(category, calender, order);
+            default:
+                throw new IllegalArgumentException("잘못된 혜택 카테고리입니다.");
+        }
+    }
+}

리뷰: 구현 조건에 switch 문은 사용 금지라고 되어 있어서...
if문으로 충분히 바꿀 수 있어 보여서 크게 중요하진 않은 것 같아요
@@ -0,0 +1,54 @@
+package christmas.domain.benefit;
+
+import christmas.domain.Calender;
+import christmas.domain.Order;
+import christmas.domain.benefit.benefitItems.BenefitItem;
+import java.util.List;
+
+public class Benefits {
+
+    public static final int GIFT_PRICE = 25_000;
+    private List<BenefitItem> benefitItems;
+
+    public Benefits(Calender calender, Order order) {
+        List<BenefitCategory> benefitCategory = BenefitCategory.getAvailableBenefitItems(calender, order);
+        this.benefitItems = BenefitItemFactory.createBenefitItems(benefitCategory, calender, order);
+    }
+
+    public List<BenefitItem> getBenefitItems() {
+        return benefitItems;
+    }
+
+    public int getBenefitPrice() {
+        return benefitItems.stream()
+                .mapToInt(BenefitItem::getDiscountPrice)
+                .sum();
+    }
+
+    public int getDiscountPrice(){
+        int sum = benefitItems.stream()
+                .mapToInt(BenefitItem::getDiscountPrice)
+                .sum();
+
+        if(isGiftBenefit())
+            sum -= GIFT_PRICE;
+
+        return sum;
+    }
+
+    public String getGiftBenefit() {
+        if(isGiftBenefit()) {
+            return "샴페인 1개";
+        }
+        return "없음";
+    }
+
+    private boolean isGiftBenefit() {
+        return benefitItems.stream()
+                .anyMatch(benefitItem -> benefitItem.getBenefitCategory() == BenefitCategory.증정이벤트);
+    }
+
+    public Badges getBadges() {
+        return Badges.getBadge(getBenefitPrice());
+    }
+}

리뷰: 증정 이벤트의 할인 금액은 사용자에게 보여주기만 하고 예상 결제 금액에는 적용되지 않기 때문에 여기서 처리해 주는게 좋은 방법 같아요!
@@ -0,0 +1,21 @@
+package christmas.domain.benefit.benefitItems;
+
+import christmas.domain.Calender;
+import christmas.domain.Order;
+import christmas.domain.benefit.BenefitCategory;
+
+public class ChristmasBenefit extends BenefitItem {
+
+    public ChristmasBenefit(BenefitCategory benefitCategory, int discountPrice) {
+        super(benefitCategory, discountPrice);
+    }
+
+    public static ChristmasBenefit of(BenefitCategory benefitCategory, Calender calender, Order order) {
+        return new ChristmasBenefit(benefitCategory, calculateDiscountPrice(benefitCategory, calender, order));
+    }
+
+    private static int calculateDiscountPrice(BenefitCategory benefitCategory, Calender calender, Order order) {
+
+        return 1000 + (calender.getDate().getDayOfMonth() - 1) * benefitCategory.getPrice();
+    }
+}

리뷰: 1000은 매직 넘버로 사용해줘도 좋을 것 같아요!
@@ -0,0 +1,65 @@
+package christmas.io;
+
+import christmas.domain.Calender;
+import christmas.domain.Order;
+import christmas.error.ErrorCode;
+import java.util.function.Supplier;
+
+public class InputView implements AutoCloseable {
+
+    private final Reader DEFAULT_READER = new ConsoleReader();
+    private final Reader reader;
+
+    public InputView(Reader reader) {
+        this.reader = reader;
+    }
+
+    public InputView() {
+        this.reader = DEFAULT_READER;
+    }
+
+    private String readLine() {
+        String input = reader.readLine();
+        validate(input);
+        return input;
+    }
+
+    private static void printReInput(IllegalArgumentException ex) {
+        System.out.flush();
+        System.out.println(ex.getMessage());
+    }
+
+    private static void validate(String input) {
+        if (input.isBlank()) {
+            throw new IllegalArgumentException(ErrorCode.BLANK_INPUT_MESSAGE.getMessage());
+        }
+    }
+
+    public Calender inputDate() {
+        return retryInput("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)",
+                () -> new Calender(readLine()));
+    }
+
+    public Order inputMenu() {
+        return retryInput("주문하실 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)",
+                () -> new Order(readLine()));
+    }
+
+
+    private static <T> T retryInput(String prompt, Supplier<T> inputSupplier) {
+        System.out.println(prompt);
+        while (true) {
+            try {
+                T result = inputSupplier.get();
+                return result;
+            } catch (IllegalArgumentException ex) {
+                printReInput(ex);
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        reader.close();
+    }
+}

리뷰: 다시 입력 받는 메서드를 제네릭과 서플라이어를 사용하여 구현하는 방식이 좋아보여요!