ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,48 @@
+package christmas.domain.plan;
+
+public enum Menu {
+
+    MUSHROOM_SOUP(6_000, Category.APPETIZER),
+    TAPAS(5_500, Category.APPETIZER),
+    CAESAR_SALAD(8_000, Category.APPETIZER),
+
+    T_BONE_STEAK(55_000, Category.MAIN),
+    BARBECUE_RIBS(54_000, Category.MAIN),
+    SEAFOOD_PASTA(35_000, Category.MAIN),
+    CHRISTMAS_PASTA(25_000, Category.MAIN),
+
+    CHOCOLATE_CAKE(15_000, Category.DESSERT),
+    ICE_CREAM(5_000, Category.DESSERT),
+
+    ZERO_COKE(3_000, Category.DRINK),
+    RED_WINE(60_000, Category.DRINK),
+    CHAMPAGNE(25_000, Category.DRINK);
+
+    private final int price;
+    private final Category category;
+
+    Menu(int price, Category category) {
+        this.price = price;
+        this.category = category;
+    }
+
+    public boolean isMain() {
+        return this.category == Category.MAIN;
+    }
+
+    public boolean isDessert() {
+        return this.category == Category.DESSERT;
+    }
+
+    public boolean isDrink() {
+        return this.category == Category.DRINK;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    private enum Category {
+        APPETIZER, MAIN, DESSERT, DRINK
+    }
+}

리뷰: 각 카테고리별로도 enum을 정의해서 관리하는 방법도 있을거라고 생각이 듭니다!
@@ -0,0 +1,27 @@
+package christmas.view;
+
+import christmas.dto.DiscountEventDto;
+import java.util.Map;
+
+class BenefitDetailView {
+
+    private static final Map<DiscountEventDto, String> DISCOUNT_EVENT_TO_MESSAGE = Map.of(
+            DiscountEventDto.CHRISTMAS_D_DAY, "크리스마스 디데이 할인", DiscountEventDto.WEEKDAY, "평일 할인",
+            DiscountEventDto.WEEKEND, "주말 할인", DiscountEventDto.SPECIAL, "특별 할인"
+    );
+    private static final String GIFT_EVENT_MESSAGE = "증정 이벤트";
+
+    private BenefitDetailView() {
+    }
+
+    public static String findView(DiscountEventDto discountEvent) {
+        if (!DISCOUNT_EVENT_TO_MESSAGE.containsKey(discountEvent)) {
+            throw new IllegalArgumentException("DiscountEventDto not enrolled at view; input : " + discountEvent);
+        }
+        return DISCOUNT_EVENT_TO_MESSAGE.get(discountEvent);
+    }
+
+    public static String findGiftEventView() {
+        return GIFT_EVENT_MESSAGE;
+    }
+}

리뷰: 세세하게 나누다보니 할인에 대한 정보를 뷰가 갖게된 것 같네요.
위에 다슬님이 말씀하신 것 처럼 뷰가 도메인에 관련한 정보를 갖게되고 변경 포인트 또한 늘지 않을까 합니다.
@@ -0,0 +1,21 @@
+package christmas.domain.discount.policy;
+
+import christmas.domain.plan.EventSchedule;
+import christmas.domain.plan.Plan;
+import java.util.Set;
+
+public class SpecialDiscount extends DiscountPolicy {
+
+    private static final EventSchedule EVENT_SCHEDULE = EventSchedule.from(Set.of(3, 10, 17, 24, 25, 31));
+    private static final int DISCOUNT_AMOUNT = 1_000;
+
+    @Override
+    boolean isSatisfyPrecondition(Plan plan) {
+        return plan.isContainedBy(EVENT_SCHEDULE);
+    }
+
+    @Override
+    int calculateDiscountAmount(Plan plan) {
+        return DISCOUNT_AMOUNT;
+    }
+}

리뷰: 특별 이벤트 날짜는 일요일 + 크리스마스날 인데, 이 부분을 명확하게 드려냈으면 더 좋지 않았을까 싶어요!
@@ -0,0 +1,47 @@
+package christmas.domain.gift;
+
+import christmas.domain.plan.Menu;
+import java.util.Map;
+import java.util.Objects;
+
+public enum Gift {
+    EVENT_GIFT(Map.of(Menu.CHAMPAGNE, 1)),
+    NOTHING(Map.of());
+
+    private static final int MIN_GIFT_PRICE = 120_000;
+
+    private final Map<Menu, Integer> menuToCount;
+
+    Gift(Map<Menu, Integer> menuToCount) {
+        this.menuToCount = Objects.requireNonNull(menuToCount);
+    }
+
+    public static Gift from(int totalPrice) {
+        if (isOverThanStandardPrice(totalPrice)) {
+            return EVENT_GIFT;
+        }
+        return NOTHING;
+    }
+
+    private static boolean isOverThanStandardPrice(int totalPrice) {
+        return totalPrice >= MIN_GIFT_PRICE;
+    }
+
+    public int calculateBenefitPrice() {
+        return menuToCount.keySet().stream()
+                .mapToInt(menu -> calculatePartOfPrice(menu, menuToCount.get(menu)))
+                .sum();
+    }
+
+    public int calculatePartOfPrice(Menu menu, int count) {
+        return menu.getPrice() * count;
+    }
+
+    public boolean isEmpty() {
+        return menuToCount.isEmpty();
+    }
+
+    public Map<Menu, Integer> getMenuToCount() {
+        return Map.copyOf(menuToCount);
+    }
+}

리뷰: 이렇게 증정 이벤트만 따로 다루셨군요 좋은 방법 알아갑니다!
@@ -0,0 +1,76 @@
+package christmas.domain.plan;
+
+import christmas.exception.DateInputException;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.IntUnaryOperator;
+
+public class EventDate {
+
+    private static final int YEAR = 2023;
+    private static final int MONTH = 12;
+    private static final int MIN_DATE = 1;
+    private static final int MAX_DATE = 31;
+
+    private static final Set<DayOfWeek> WEEKEND = Set.of(DayOfWeek.FRIDAY, DayOfWeek.SATURDAY);
+
+    private final LocalDate date;
+
+    private EventDate(int date) {
+        validate(date);
+        this.date = LocalDate.of(YEAR, MONTH, date);
+    }
+
+    private static void validate(int date) {
+        if (isOutOfRange(date)) {
+            throw new DateInputException("Date is Out of Range : " + date);
+        }
+    }
+
+    private static boolean isOutOfRange(int date) {
+        return date < MIN_DATE || date > MAX_DATE;
+    }
+
+    public static EventDate from(int date) {
+        return new EventDate(date);
+    }
+
+    public boolean isWeekend() {
+        DayOfWeek dayOfWeek = date.getDayOfWeek();
+        return WEEKEND.contains(dayOfWeek);
+    }
+
+    public boolean isWeekDay() {
+        return !isWeekend();
+    }
+
+    public int calculateByDate(IntUnaryOperator function) {
+        return function.applyAsInt(getDate());
+    }
+
+    public int getDate() {
+        return date.getDayOfMonth();
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+        if (object == null || getClass() != object.getClass()) {
+            return false;
+        }
+        EventDate comparedDate = (EventDate) object;
+        return Objects.equals(date, comparedDate.date);
+    }
+
+    @Override
+    public int hashCode() {
+        if (date == null) {
+            return 0;
+        }
+        return date.hashCode();
+    }
+}

리뷰: 저는 LocalDate를 그냥 사용했었는데, 이렇게 따로 클래스로 파서 다루는게 훨씬 깔끔한것 같네요
@@ -0,0 +1,79 @@
+package christmas.controller;
+
+import christmas.domain.badge.Badge;
+import christmas.domain.discount.DiscountDetails;
+import christmas.domain.discount.TotalDiscountEvent;
+import christmas.domain.gift.Gift;
+import christmas.domain.plan.EventDate;
+import christmas.domain.plan.Menu;
+import christmas.domain.plan.Order;
+import christmas.domain.plan.Plan;
+import christmas.dto.PlanResultDto;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+import java.util.Map;
+import java.util.function.Supplier;
+
+public class ChristmasPlanerController {
+
+    private final TotalDiscountEvent discountEvent = TotalDiscountEvent.createDecemberEvent();
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+
+    public void run() {
+        startApplication();
+        Plan plan = inputPlan();
+        PlanResultDto planResult = applyEvents(plan);
+        printEventResult(planResult);
+    }
+
+    private void startApplication() {
+        outputView.printApplicationTitle();
+    }
+
+    private Plan inputPlan() {
+        EventDate date = readRepeatedlyUntilNoException(this::readDate);
+        Order order = readRepeatedlyUntilNoException(this::readOrder);
+        return Plan.of(date, order);
+    }
+
+    private <T> T readRepeatedlyUntilNoException(Supplier<T> supplier) {
+        try {
+            return supplier.get();
+        } catch (IllegalArgumentException exception) {
+            outputView.printExceptionMessage(exception);
+            return readRepeatedlyUntilNoException(supplier);
+        }
+    }
+
+    private EventDate readDate() {
+        int date = inputView.readDate();
+        return EventDate.from(date);
+    }
+
+    private Order readOrder() {
+        Map<Menu, Integer> order = inputView.readOrder();
+        return Order.from(order);
+    }
+
+    private PlanResultDto applyEvents(Plan plan) {
+        DiscountDetails discountDetails = discountEvent.makeDiscountDetails(plan);
+        Gift gift = Gift.from(plan.calculateTotalPrice());
+        int totalBenefitPrice = calculateTotalBenefitPrice(discountDetails, gift);
+        Badge badge = Badge.from(totalBenefitPrice);
+
+        return PlanResultDto.builder()
+                .plan(plan).discountDetails(discountDetails)
+                .gift(gift).badge(badge).build();
+    }
+
+    private int calculateTotalBenefitPrice(DiscountDetails discountDetails, Gift gift) {
+        int totalDiscountPrice = discountDetails.calculateTotalDiscountPrice();
+        int giftBenefitPrice = gift.calculateBenefitPrice();
+        return totalDiscountPrice + giftBenefitPrice;
+    }
+
+    private void printEventResult(PlanResultDto planResult) {
+        outputView.printPlanResult(planResult);
+    }
+}

리뷰: 아마도 실질적으로는 프로그램이 작은 크기이다 보니 Service Layer로까지 분리할 필요성을 못느낀 것 같습니다. 메서드를 분리하는 것 만으로도 충분하다고 생각했습니다. 만약 여기에서 프로그램이 점점 커진다면 Service Layer가 필요할 것 같습니다.
@@ -0,0 +1,48 @@
+package christmas.domain.plan;
+
+public enum Menu {
+
+    MUSHROOM_SOUP(6_000, Category.APPETIZER),
+    TAPAS(5_500, Category.APPETIZER),
+    CAESAR_SALAD(8_000, Category.APPETIZER),
+
+    T_BONE_STEAK(55_000, Category.MAIN),
+    BARBECUE_RIBS(54_000, Category.MAIN),
+    SEAFOOD_PASTA(35_000, Category.MAIN),
+    CHRISTMAS_PASTA(25_000, Category.MAIN),
+
+    CHOCOLATE_CAKE(15_000, Category.DESSERT),
+    ICE_CREAM(5_000, Category.DESSERT),
+
+    ZERO_COKE(3_000, Category.DRINK),
+    RED_WINE(60_000, Category.DRINK),
+    CHAMPAGNE(25_000, Category.DRINK);
+
+    private final int price;
+    private final Category category;
+
+    Menu(int price, Category category) {
+        this.price = price;
+        this.category = category;
+    }
+
+    public boolean isMain() {
+        return this.category == Category.MAIN;
+    }
+
+    public boolean isDessert() {
+        return this.category == Category.DESSERT;
+    }
+
+    public boolean isDrink() {
+        return this.category == Category.DRINK;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    private enum Category {
+        APPETIZER, MAIN, DESSERT, DRINK
+    }
+}

리뷰: 저는 Menu Enum에게서 public method를 제공해야 하기 때문에 이 방식이 효율적이라고 생각했습니다. 각 카테고리별로도 enum을 관리하는 코드는 어떤 느낌일까요? 그리고 그 방식이 이 방식보다 더 좋은 점은 무엇이 있을까요?
@@ -0,0 +1,27 @@
+package christmas.view;
+
+import christmas.dto.DiscountEventDto;
+import java.util.Map;
+
+class BenefitDetailView {
+
+    private static final Map<DiscountEventDto, String> DISCOUNT_EVENT_TO_MESSAGE = Map.of(
+            DiscountEventDto.CHRISTMAS_D_DAY, "크리스마스 디데이 할인", DiscountEventDto.WEEKDAY, "평일 할인",
+            DiscountEventDto.WEEKEND, "주말 할인", DiscountEventDto.SPECIAL, "특별 할인"
+    );
+    private static final String GIFT_EVENT_MESSAGE = "증정 이벤트";
+
+    private BenefitDetailView() {
+    }
+
+    public static String findView(DiscountEventDto discountEvent) {
+        if (!DISCOUNT_EVENT_TO_MESSAGE.containsKey(discountEvent)) {
+            throw new IllegalArgumentException("DiscountEventDto not enrolled at view; input : " + discountEvent);
+        }
+        return DISCOUNT_EVENT_TO_MESSAGE.get(discountEvent);
+    }
+
+    public static String findGiftEventView() {
+        return GIFT_EVENT_MESSAGE;
+    }
+}

리뷰: 변경 포인트가 늘어난다는 점, 흥미롭네요. 확실히 정책이 하나 더 추가된다면, enum에도 추가하고 BenefitDetailView에도  추가되어야 하니 좀 그렇네요. 이걸 구현할 때 제 생각은 이랬습니다.

> 만약에 "크리스마스 디데이 할인"이라는 출력 텍스트를 "크리스마스 D-day 할인"으로 수정한다고 하면, 해당 코드는 어디에 있어야 할까?

이 질문에서 출발했습니다. 저는 '출력 형식을 수정'하는 것이기 때문에  view package에 있어야 한다고 생각했습니다. 출력 형식이 도메인에 따라 여기 저기에 퍼져있는 것도 수정하는데 별로라고 생각합니다. 그래서 이 부분은 앞으로 어떻게 변경될 가능성이 있는지에 따라 다르게 구현할 것 같습니다.
@@ -0,0 +1,33 @@
+import letters from "../mockData";
+
+const sleep = n => new Promise(resolve => setTimeout(resolve, n));
+
+export const getLetters = async () => {
+  await sleep(500);
+  return letters;
+};
+
+export const getLetterById = async id => {
+  await sleep(100);
+  return letters.find(letter => letter.id === id);
+};
+
+export const updateLetter = async (id, payload) => {
+  await sleep(100);
+  const { title, artist, imageUrl } = payload;
+  const songStory = payload.songStory;
+
+  const foundLetter = letters.find(letter => letter.id === id);
+
+  const updatedLetter = {
+    ...foundLetter,
+    song: { title, artist, imageUrl },
+    songStory
+  };
+
+  const index = letters.indexOf(foundLetter);
+  letters.splice(index, 1);
+  letters.push(updatedLetter);
+
+  return updatedLetter;
+};

리뷰: 배열에서 원하는 조건까지만 찾는 find method를 사용한 건 좋아보입니다 !
@@ -0,0 +1,24 @@
+import React from "react";
+import styled from "styled-components";
+
+const Footer = () => {
+  return (
+    <FooterBlock>
+      <p className="copyright">&copy; Museop Kim</p>
+    </FooterBlock>
+  );
+};
+
+const FooterBlock = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 8rem;
+  margin-top: 6rem;
+  background-color: #f5f5f7;
+  font-weight: 500;
+  box-shadow: 0px -10px 35px 5px #f1f1f1;
+`;
+
+export default Footer;

리뷰: style components를 사용하시니까

`box-shadow: 0px -10px 35px 5px #f1f1f1;`

부분의 색상 같은 경우는 styled-components ThemeProvider를 사용해 보는건 어떨까요.
모든 컴포넌트가 전달받은 색상을 동일하게 사용하면 다크모드와 같은 기능을 구현할때도 손쉽게 스타일 변동이 가능합니다.
너무 많이 진행된 프로젝트는 스타일 시스템을 구축하기 쉽지 않으니까 좀 작은 단위일 때 시도해 보는것도 좋을거 같아요 😉
@@ -0,0 +1,47 @@
+import React from "react";
+import styled from "styled-components";
+import { Link } from "react-router-dom";
+
+const Navigation = () => {
+  return (
+    <NavigationBlock>
+      <Link to="/">신청곡</Link>
+      <Link to="/ranking">신청곡 순위</Link>
+      <Link to="/contents">신청곡 유튜브 영상</Link>
+      <Link to="/musicsheets">리얼피아노 악보집</Link>
+    </NavigationBlock>
+  );
+};
+
+const NavigationBlock = styled.div`
+  height: 100%;
+  display: flex;
+  align-items: center;
+
+  & > * {
+    /* padding: 25px 5px 10px 5px; */
+    box-sizing: border-box;
+    display: flex;
+    align-items: center;
+    margin: 0px 30px;
+    padding-top: 10px;
+    font-size: 1.2rem;
+    font-weight: 600;
+    text-decoration: none;
+    align-self: center;
+    color: inherit;
+    opacity: 0.5;
+    height: 100%;
+    /* border-bottom: 1px solid rgba(0, 0, 0, 0); */
+    border-bottom: 3px solid rgba(250, 162, 193, 0);
+    transition: 0.3s;
+
+    :hover {
+      opacity: 1;
+      /* color: #faa2c1; */
+      border-bottom: 3px solid rgba(250, 162, 193, 1);
+    }
+  }
+`;
+
+export default Navigation;

리뷰: 폰트와 같이 반응형에 영향을 받아야 하는 기준과 그렇지 않은 요소를 기준으로 px과 rem을 사용하시는 건가요 ?
그런 의도라면 좋아보입니다.
@@ -0,0 +1,29 @@
+import React from "react";
+import styled from "styled-components";
+import LetterContainer from "../../containers/Letter/LetterContainer";
+
+const LetterList = ({ letters }) => {
+  return (
+    <LetterListBlock>
+      {letters.map(letter => (
+        <LetterContainer
+          key={letter.id}
+          id={letter.id}
+          user={letter.user}
+          song={letter.song}
+          songStory={letter.songStory}
+          createdDateTime={letter.createdDateTime}
+        />
+      ))}
+    </LetterListBlock>
+  );
+};
+
+const LetterListBlock = styled.ul`
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 4.5rem;
+`;
+
+export default LetterList;

리뷰: 보통 Container는 좀 더 큰 단위에서의 의미에서 사용되곤 하는거 같아요.
여러 자식 컴포넌트를 가지고 많은 이벤트 로직을 가지고 있는 센터의 느낌으로 네이밍을 하곤 하는데
LetterList면 LetterItem과 같이 단일하게 표현해 주는것도 좋을거 같아요 !
@@ -0,0 +1,30 @@
+import React, { useEffect, useState } from "react";
+import ModalTemplate from "../Template/Modal/ModalTemplate";
+import LetterDetailsSong from "./LetterDetailsSong";
+import LetterDetailsSongStory from "./LetterDetailsSongStory";
+import LetterModalTemplate from "../Template/LetterModal/LetterModalTemplate";
+import LetterDetailsUser from "./LetterDetailsUser";
+import LetterModalDiv from "../LetterModal/LetterModalContents/LetterModalDiv";
+import LetterModalHiddenButtonContainer from "../../containers/LetterModal/LetterModalHiddenButtonContainer";
+import LetterModalButtonContainer from "../../containers/LetterModal/LetterModalButtonContainer";
+import { useSelector } from "react-redux";
+
+const LetterDetails = ({ letter }) => {
+  const { song, songStory, createdDateTime, user } = letter;
+
+  return (
+    <ModalTemplate>
+      <LetterModalTemplate>
+        <LetterModalHiddenButtonContainer />
+        <LetterModalDiv>
+          <LetterDetailsSong song={song} />
+          <LetterDetailsSongStory songStory={songStory} />
+          <LetterDetailsUser user={user} createdDateTime={createdDateTime} />
+          <LetterModalButtonContainer />
+        </LetterModalDiv>
+      </LetterModalTemplate>
+    </ModalTemplate>
+  );
+};
+
+export default LetterDetails;

리뷰: `const { song, songStory, createdDateTime, user } = letter;`

너무 많은 양의 객체 Props가 아니라면 이 부분 같은 경우는 각각 이름을 가진 Props로 전달해 주는것도 좋을거 같다는 생각이 듭니다! 그러면 간단하게 컴포넌트에 직접 들어와 보지 않아도 전달되는 Props 만으로 역할을 유추해 볼 수 있을거 같아요.
@@ -0,0 +1,45 @@
+import React, { useEffect } from "react";
+import { MdMoreHoriz } from "react-icons/md";
+import styled, { css } from "styled-components";
+import LetterDetailsHiddenMenuButton from "./LetterDetailsHiddenMenuButton";
+
+const LetterDetailsHiddenMenu = ({
+  isMouseEnter,
+  isMenuOpen,
+  onToggle,
+  changeToEdit
+}) => {
+  return (
+    <ButtonBlock isMouseEnter={isMouseEnter} onClick={onToggle}>
+      <HiddenMenu />
+      {isMenuOpen && (
+        <LetterDetailsHiddenMenuButton changeToEdit={changeToEdit} />
+      )}
+    </ButtonBlock>
+  );
+};
+
+const ButtonBlock = styled.div`
+  ${props =>
+    props.isMouseEnter
+      ? css`
+          visibility: visible;
+          opacity: 1;
+        `
+      : css`
+          visibility: hidden;
+          opacity: 0;
+        `}
+  transition: 0.7s;
+`;
+
+const HiddenMenu = styled(MdMoreHoriz)`
+  position: absolute;
+  bottom: 93%;
+  right: 3%;
+  cursor: pointer;
+  font-size: 2.1rem;
+  color: gray;
+`;
+
+export default LetterDetailsHiddenMenu;

리뷰: visibility: hidden; , display : none 의 차이를 아시나요 ? ㅎㅎ
모르고 계신다면 한번 알아보는것도 좋습니다 ! 대단한건 아니고 그냥 DOM 트리와 관련된 내용이에요.
@@ -0,0 +1,28 @@
+import React from "react";
+import LetterModalSong from "../LetterModal/LetterModalSong/LetterModalSong";
+import SongAbout from "../LetterModal/LetterModalSong/SongAbout";
+import SongArticle from "../LetterModal/LetterModalSong/SongArticle";
+import SongArticleItem from "../LetterModal/LetterModalSong/SongArticleItem";
+import SongArticleName from "../LetterModal/LetterModalSong/SongArticleName";
+import SongImage from "../LetterModal/LetterModalSong/SongImage";
+
+const LetterDetailsSong = ({ song }) => {
+  const { title, artist, imageUrl } = song;
+  return (
+    <LetterModalSong>
+      <SongImage imageUrl={imageUrl} />
+      <SongAbout>
+        <SongArticle>
+          <SongArticleName articleName={"TITLE"} />
+          <SongArticleItem articleName={"TITLE"} item={title} />
+        </SongArticle>
+        <SongArticle>
+          <SongArticleName articleName={"ARTIST"} />
+          <SongArticleItem articleName={"ARTIST"} item={artist} />
+        </SongArticle>
+      </SongAbout>
+    </LetterModalSong>
+  );
+};
+
+export default LetterDetailsSong;

리뷰: 이 부분의 경우 반복되는 컴포넌트로 보이는데 이걸 하나로 묶어서 관리해 보는건 어떨까요 ?
        
        <SongArticle>
          <SongArticleName articleName={"TITLE"} />
          <SongArticleItem articleName={"TITLE"} item={title} />
        </SongArticle>
@@ -0,0 +1,48 @@
+import React from "react";
+import styled from "styled-components";
+import { BiSearch } from "react-icons/bi";
+import useModal from "../../hooks/useModal";
+import SongSearchModal from "./SongSearchModal/SongSearchModal";
+
+const LetterEditorSearchButton = () => {
+  const [isOpened, onOpenModal, onCloseModal] = useModal();
+
+  return (
+    <>
+      <StyledButton onClick={onOpenModal}>
+        <SearchIcon />
+        신청곡 검색
+      </StyledButton>
+      <SongSearchModal isOpened={isOpened} onCloseModal={onCloseModal} />
+    </>
+  );
+};
+
+const StyledButton = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: absolute;
+  width: 11rem;
+  top: 14%;
+  right: 15%;
+
+  padding: 0.35rem 0rem;
+  border-style: none;
+  border-radius: 0.5rem;
+  font-size: 1.2rem;
+  background-color: #f06595;
+  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.15);
+  color: #fff;
+  font-weight: 500;
+  z-index: 1;
+  cursor: pointer;
+`;
+
+const SearchIcon = styled(BiSearch)`
+  font-size: 1.35rem;
+  margin-right: 0.35rem;
+  opacity: 0.9;
+`;
+
+export default LetterEditorSearchButton;

리뷰: modal과 관련된 동작을 custom hook으로 사용한 부분 좋아 보입니다 !
@@ -0,0 +1,108 @@
+import React from "react";
+import styled from "styled-components";
+import { BiSearch } from "react-icons/bi";
+import { useDispatch } from "react-redux";
+import { searchSong } from "../../../modules/song";
+
+const SearchForm = ({ onCloseModal }) => {
+  const dispatch = useDispatch();
+
+  const onSearchSong = event => {
+    event.preventDefault();
+    dispatch(searchSong());
+  };
+
+  return (
+    <Form>
+      <SearchInputWrap>
+        <SearchLabel>아티스트</SearchLabel>
+        <SearchInput type="text" name="artist" />
+      </SearchInputWrap>
+      <SearchInputWrap>
+        <SearchLabel>제목</SearchLabel>
+        <SearchInput type="text" name="title" />
+      </SearchInputWrap>
+      <SearchButton type="submit" onClick={onSearchSong}>
+        <SearchIcon />
+      </SearchButton>
+    </Form>
+  );
+};
+
+const Form = styled.form`
+  box-sizing: border-box;
+  width: 95%;
+  display: flex;
+  justify-content: center;
+  margin: 0rem auto 3.5rem auto;
+`;
+
+const SearchLabel = styled.span`
+  display: flex;
+  align-items: center;
+  position: absolute;
+  top: 0rem;
+  left: 0rem;
+  height: 2.37rem;
+  font-size: 1rem;
+  background-color: #727272;
+  padding: 0rem 0.7rem;
+  border-radius: 0.3rem 0rem 0rem 0.3rem;
+  color: white;
+  font-weight: 600;
+  box-shadow: 1px 0px 3px 1px rgba(0, 0, 0, 0.1);
+`;
+
+const SearchInput = styled.input`
+  border: none;
+  outline: none;
+  color: #868e96;
+  width: 100%;
+  margin: 0rem 0.5rem 0rem 5.5rem;
+  font-size: 1.2rem;
+  background-color: #fbfbfd;
+`;
+
+const SearchInputWrap = styled.div`
+  display: flex;
+  align-items: center;
+  position: relative;
+  border: 1px solid #e9ecef;
+  border-radius: 1rem 0rem 0rem 1rem;
+  padding: 0.1rem 0rem;
+  background: #fbfbfd;
+  height: 2.1rem;
+  flex: 11;
+
+  &:nth-child(2) {
+    & > ${SearchLabel} {
+      border-radius: 0.2rem;
+      margin-left: -0.25rem;
+    }
+
+    & > ${SearchInput} {
+      margin-left: 3.8rem;
+    }
+  }
+`;
+
+const SearchButton = styled.button`
+  display: flex;
+  position: relative;
+  justify-content: center;
+  align-items: center;
+  border: none;
+  outline: none;
+  cursor: pointer;
+  background-color: #e1999c;
+  border-radius: 0rem 0.5rem 0.5rem 0rem;
+  flex: 1;
+  box-shadow: 1px 0px 3px 1px rgba(0, 0, 0, 0.1);
+`;
+
+const SearchIcon = styled(BiSearch)`
+  color: #fff;
+  font-size: 1.4rem;
+`;
+
+export default SearchForm;

리뷰: Form이 점점 더 복잡한 상태를 가지면 관리가 쉽지 않은데 그럴땐
https://react-hook-form.com/
위와 같은 도구를 도입해 볼 수도 있습니다 ㅎ
@@ -0,0 +1,24 @@
+import React from "react";
+import styled from "styled-components";
+
+const Footer = () => {
+  return (
+    <FooterBlock>
+      <p className="copyright">&copy; Museop Kim</p>
+    </FooterBlock>
+  );
+};
+
+const FooterBlock = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 8rem;
+  margin-top: 6rem;
+  background-color: #f5f5f7;
+  font-weight: 500;
+  box-shadow: 0px -10px 35px 5px #f1f1f1;
+`;
+
+export default Footer;

리뷰: 컴포넌트를 재사용 하듯이 스타일도 여러 곳에 재사용 하는 방법이 있었네요. `ThemeProvider`에 대해 잘 몰랐는데 이번 기회에 한번 학습 해보고 해볼 수 있다면 적용까지 해보겠습니다! 조언 해주셔서 감사합니다 👍