ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+REPOSITORY=/home/ubuntu/app/songrequest
+PROJECT_NAME=song-request
+
+echo "> Build 파일 복사"
+
+cp $REPOSITORY/zip/*.jar $REPOSITORY/
+
+echo "> 현재 구동 중인 애플리케이션 pid 확인"
+
+CURRENT_PID=$(pgrep -fl song-request | grep jar | awk '{print $1}')
+
+echo "현재 구동 중인 애플리케이션 pid : $CURRENT_PID"
+
+if [ -z "$CURRENT_PID" ]; then
+  echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
+else
+  echo "> kill -15 $CURRENT_PID"
+  kill -15 $CURRENT_PID
+  sleep 5
+fi
+
+echo "> 새 애플리케이션 배포"
+
+JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
+
+echo "> JAR Name : $JAR_NAME"
+
+echo "> $JAR_NAME에 실행 권한 추가"
+
+chmod +x $JAR_NAME
+
+echo "> $JAR_NAME 실행"
+
+nohup java -jar \
+  -Dspring.profiles.active=prod \
+  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

리뷰: 말씀 하신 것처럼 EC2와 Nginx를 설정하고 배포 환경을 구성하는 과정에서 리눅스 명령어가 많이 부족하다는 것을 느꼈습니다.
그래서 별도로 조금씩 공부를 하고 있어요ㅎㅎ 조언 해주셔서 감사합니다! 🙂
@@ -0,0 +1,49 @@
+import {
+  CHRISTMAS_DISCOUNT,
+  WEEKDAY_DISCOUNT,
+  WEEKEND_DISCOUNT,
+  SPECIAL_DISCOUNT,
+  GIFT_DISCOUNT,
+} from '../utils/constants/discount';
+
+class Discount {
+  static getChristmasDiscount(visitDate) {
+    if (visitDate.isChristmasDay()) {
+      const date = visitDate.getVisitDate();
+      const incrementPrice = (date - CHRISTMAS_DISCOUNT.startDate) * CHRISTMAS_DISCOUNT.incrementPrice;
+
+      return CHRISTMAS_DISCOUNT.defaultPrice + incrementPrice;
+    }
+    return 0;
+  }
+
+  static getWeekdayDiscount(visitDate, count) {
+    if (visitDate.isWeekday()) {
+      return WEEKDAY_DISCOUNT.price * count;
+    }
+    return 0;
+  }
+
+  static getWeekendDiscount(visitDate, count) {
+    if (visitDate.isWeekend()) {
+      return WEEKEND_DISCOUNT.price * count;
+    }
+    return 0;
+  }
+
+  static getSpecialDiscount(visitDate) {
+    if (visitDate.isSpecialDay()) {
+      return SPECIAL_DISCOUNT.price;
+    }
+    return 0;
+  }
+
+  static getGiftDiscount(totalPrice) {
+    if (totalPrice >= GIFT_DISCOUNT.triggerPrice) {
+      return GIFT_DISCOUNT.price * GIFT_DISCOUNT.count;
+    }
+    return 0;
+  }
+}
+
+export default Discount;

리뷰: 방문 날짜라는 객체를 따로 만들어 관리하셨군요..! 정말 좋은 아이디어인 것 같습니다. 배워갑니다.
@@ -0,0 +1,59 @@
+import Discount from './Discount';
+import Order from './Order';
+import VisitDate from './VisitDate';
+import { BADGE, EVENT } from '../utils/constants/event';
+
+class Planner {
+  #visitDate;
+  #order;
+  #price;
+
+  setVisitDate(visitDate) {
+    this.#visitDate = new VisitDate(visitDate);
+  }
+
+  setOrder(order) {
+    this.#order = new Order(order);
+    this.#price = Array.from(this.#order.getOrder().entries()).reduce(
+      (price, [menu, count]) => price + menu.getPrice() * count,
+      0,
+    );
+  }
+
+  getVisitDate() {
+    return this.#visitDate.getVisitDate();
+  }
+
+  getOrder() {
+    return this.#order.getOrder();
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  caculateDiscount() {
+    if (this.#price < EVENT.discountTriggerPrice) {
+      return [{}, 0];
+    }
+    const discount = {
+      christmas: Discount.getChristmasDiscount(this.#visitDate),
+      weekday: Discount.getWeekdayDiscount(this.#visitDate, this.#order.getWeekdayMenuCount()),
+      weekend: Discount.getWeekendDiscount(this.#visitDate, this.#order.getWeekendMenuCount()),
+      special: Discount.getSpecialDiscount(this.#visitDate),
+      gift: Discount.getGiftDiscount(this.#price),
+    };
+    const discountPrice = Object.values(discount).reduce((total, discount) => total + discount, 0);
+    this.#price -= discountPrice - discount.gift;
+
+    return [discount, discountPrice];
+  }
+
+  getBadge(discountPrice) {
+    const badge = Object.values(BADGE).find(({ price }) => discountPrice >= price);
+
+    return badge?.name;
+  }
+}
+
+export default Planner;

리뷰: 저는 if 문으로 처리했는데, 이런 방법도 있네요!
@@ -0,0 +1,34 @@
+import { MENU } from '../utils/constants/menu';
+import { validateMenuName } from '../utils/validators/validateMenu';
+
+class Menu {
+  #name;
+  #price;
+  #type;
+
+  constructor(name) {
+    validateMenuName(name);
+    this.#name = name;
+
+    Object.entries(MENU).forEach(([type, menus]) => {
+      if (menus.hasOwnProperty(name)) {
+        this.#price = menus[name];
+        this.#type = type;
+      }
+    });
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  getName() {
+    return this.#name;
+  }
+
+  getType() {
+    return this.#type;
+  }
+}
+
+export default Menu;

리뷰: ```
// bad
console.log(object.hasOwnProperty(key));

// good
console.log(Object.prototype.hasOwnProperty.call(object, key));

// better
const has = Object.prototype.hasOwnProperty; // cache the lookup once, in module scope.
console.log(has.call(object, key));

// best
console.log(Object.hasOwn(object, key)); // only supported in browsers that support ES2022

/* or */
import has from 'has'; // https://www.npmjs.com/package/has
console.log(has(object, key));
/* or */
console.log(Object.hasOwn(object, key)); // https://www.npmjs.com/package/object.hasown
```
----------------------------------------------------------------------------------------------

Airbnb 가이드에 따르면 hasOwnProperty 를 지양하고 있습니다.

`menus.hasOwnProperty(name)` 대신 `Object.hasOwn(menus, name)` 는 어떨까요?

`hasOwnProperty`를 지양하는 이유에 대해선 아래 링크를 참조하면 좋을 것 같습니다.

https://velog.io/@jay/be-carefule-use-hasownproperty
@@ -0,0 +1,47 @@
+import Menu from './Menu';
+import { WEEKDAY_DISCOUNT, WEEKEND_DISCOUNT } from '../utils/constants/discount';
+import { DELIMITER } from '../utils/constants/event';
+import { validateDuplicateOrder, validateOnlyDrink, validateTotalMenuCount } from '../utils/validators/vaildateOrder';
+import { validateMenuCount } from '../utils/validators/validateMenu';
+
+class Order {
+  #order;
+
+  constructor(orderInput) {
+    this.#order = new Map();
+
+    orderInput.split(DELIMITER.orderDelimiter).forEach((menu) => {
+      this.#addMenu(menu);
+    });
+
+    validateOnlyDrink(this.#order);
+    validateTotalMenuCount(this.#order);
+  }
+
+  #addMenu(menu) {
+    const [name, count] = menu.split(DELIMITER.menuDelimiter);
+
+    if (validateDuplicateOrder(name, this.#order)) {
+      validateMenuCount(Number(count));
+      this.#order.set(new Menu(name), Number(count));
+    }
+  }
+
+  getWeekdayMenuCount() {
+    return Array.from(this.#order.entries())
+      .filter(([menu]) => WEEKDAY_DISCOUNT.menuType === menu.getType())
+      .reduce((totalCount, [_, count]) => totalCount + count, 0);
+  }
+
+  getWeekendMenuCount() {
+    return Array.from(this.#order.entries())
+      .filter(([menu]) => WEEKEND_DISCOUNT.menuType === menu.getType())
+      .reduce((totalCount, [_, count]) => totalCount + count, 0);
+  }
+
+  getOrder() {
+    return this.#order;
+  }
+}
+
+export default Order;

리뷰: 구조 분해 할당을 왜 생각 못 했을까요.. split 해서 array[0]  array[1] 해서 하나씩 담았네요 ㅠㅠ
@@ -0,0 +1,59 @@
+import Discount from './Discount';
+import Order from './Order';
+import VisitDate from './VisitDate';
+import { BADGE, EVENT } from '../utils/constants/event';
+
+class Planner {
+  #visitDate;
+  #order;
+  #price;
+
+  setVisitDate(visitDate) {
+    this.#visitDate = new VisitDate(visitDate);
+  }
+
+  setOrder(order) {
+    this.#order = new Order(order);
+    this.#price = Array.from(this.#order.getOrder().entries()).reduce(
+      (price, [menu, count]) => price + menu.getPrice() * count,
+      0,
+    );
+  }
+
+  getVisitDate() {
+    return this.#visitDate.getVisitDate();
+  }
+
+  getOrder() {
+    return this.#order.getOrder();
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  caculateDiscount() {
+    if (this.#price < EVENT.discountTriggerPrice) {
+      return [{}, 0];
+    }
+    const discount = {
+      christmas: Discount.getChristmasDiscount(this.#visitDate),
+      weekday: Discount.getWeekdayDiscount(this.#visitDate, this.#order.getWeekdayMenuCount()),
+      weekend: Discount.getWeekendDiscount(this.#visitDate, this.#order.getWeekendMenuCount()),
+      special: Discount.getSpecialDiscount(this.#visitDate),
+      gift: Discount.getGiftDiscount(this.#price),
+    };
+    const discountPrice = Object.values(discount).reduce((total, discount) => total + discount, 0);
+    this.#price -= discountPrice - discount.gift;
+
+    return [discount, discountPrice];
+  }
+
+  getBadge(discountPrice) {
+    const badge = Object.values(BADGE).find(({ price }) => discountPrice >= price);
+
+    return badge?.name;
+  }
+}
+
+export default Planner;

리뷰: 저도 if문으로 처리했는데.. 감동 받고 갑니다
@@ -0,0 +1,4 @@
+export const priceFormatter = new Intl.NumberFormat('ko-KR', {
+  minimumFractionDigits: 0,
+  maximumFractionDigits: 0,
+});

리뷰: 0, 0 을 세팅하는 것이 어떤 작업인지 궁금합니다..!
@@ -0,0 +1,20 @@
+import { VISIT_DATE_MESSAGE } from '../messages/errorMessages';
+import { EVENT } from '../constants/event';
+import CustomError from '../errors/CustomError';
+
+export const validateVisitDate = (visitDate) => {
+  validateType(visitDate);
+  validateRange(visitDate);
+};
+
+const validateType = (visitDate) => {
+  if (typeof visitDate !== 'number' || Number.isNaN(visitDate)) {
+    throw new CustomError(VISIT_DATE_MESSAGE.invalidNumber);
+  }
+};
+
+const validateRange = (visitDate) => {
+  if (visitDate < EVENT.startDate || visitDate > EVENT.endDate) {
+    throw new CustomError(VISIT_DATE_MESSAGE.invalidRange);
+  }
+};

리뷰: 해당 날짜 유효성 검증에서  1.5 같은 Integer 형식의 경우 필터링이 되는지 궁금합니다.
@@ -0,0 +1,36 @@
+import { CHRISTMAS_DISCOUNT, SPECIAL_DISCOUNT } from '../utils/constants/discount';
+import { EVENT } from '../utils/constants/event';
+import { validateVisitDate } from '../utils/validators/validateVisitDate';
+
+class VisitDate {
+  #visitDate;
+
+  constructor(dateInput) {
+    const visitDate = Number(dateInput);
+    validateVisitDate(visitDate);
+    this.#visitDate = visitDate;
+  }
+
+  getVisitDate() {
+    return this.#visitDate;
+  }
+
+  isWeekend() {
+    const localDate = new Date(EVENT.year, 11, this.#visitDate);
+    return localDate.getDay() === 5 || localDate.getDay() === 6;
+  }
+
+  isWeekday() {
+    return !this.isWeekend();
+  }
+
+  isSpecialDay() {
+    return SPECIAL_DISCOUNT.starDates.includes(this.#visitDate);
+  }
+
+  isChristmasDay() {
+    return this.#visitDate >= CHRISTMAS_DISCOUNT.startDate && this.#visitDate <= CHRISTMAS_DISCOUNT.endDate;
+  }
+}
+
+export default VisitDate;

리뷰: 저는 이벤트 처리와 VisitDate 로직을 묶어서 구현 했는데 다시 생각 해보니 따로 분리해 두는게 나중에 추가 이벤트가 발생 하였을 때 대응하기 좋을 것 같다는 생각이 듭니다!
@@ -0,0 +1,59 @@
+import Discount from './Discount';
+import Order from './Order';
+import VisitDate from './VisitDate';
+import { BADGE, EVENT } from '../utils/constants/event';
+
+class Planner {
+  #visitDate;
+  #order;
+  #price;
+
+  setVisitDate(visitDate) {
+    this.#visitDate = new VisitDate(visitDate);
+  }
+
+  setOrder(order) {
+    this.#order = new Order(order);
+    this.#price = Array.from(this.#order.getOrder().entries()).reduce(
+      (price, [menu, count]) => price + menu.getPrice() * count,
+      0,
+    );
+  }
+
+  getVisitDate() {
+    return this.#visitDate.getVisitDate();
+  }
+
+  getOrder() {
+    return this.#order.getOrder();
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  caculateDiscount() {
+    if (this.#price < EVENT.discountTriggerPrice) {
+      return [{}, 0];
+    }
+    const discount = {
+      christmas: Discount.getChristmasDiscount(this.#visitDate),
+      weekday: Discount.getWeekdayDiscount(this.#visitDate, this.#order.getWeekdayMenuCount()),
+      weekend: Discount.getWeekendDiscount(this.#visitDate, this.#order.getWeekendMenuCount()),
+      special: Discount.getSpecialDiscount(this.#visitDate),
+      gift: Discount.getGiftDiscount(this.#price),
+    };
+    const discountPrice = Object.values(discount).reduce((total, discount) => total + discount, 0);
+    this.#price -= discountPrice - discount.gift;
+
+    return [discount, discountPrice];
+  }
+
+  getBadge(discountPrice) {
+    const badge = Object.values(BADGE).find(({ price }) => discountPrice >= price);
+
+    return badge?.name;
+  }
+}
+
+export default Planner;

리뷰: 혹시 물음표에 해당하는게 어떤 문법인지 궁금합니다.
@@ -0,0 +1,49 @@
+import {
+  CHRISTMAS_DISCOUNT,
+  WEEKDAY_DISCOUNT,
+  WEEKEND_DISCOUNT,
+  SPECIAL_DISCOUNT,
+  GIFT_DISCOUNT,
+} from '../utils/constants/discount';
+
+class Discount {
+  static getChristmasDiscount(visitDate) {
+    if (visitDate.isChristmasDay()) {
+      const date = visitDate.getVisitDate();
+      const incrementPrice = (date - CHRISTMAS_DISCOUNT.startDate) * CHRISTMAS_DISCOUNT.incrementPrice;
+
+      return CHRISTMAS_DISCOUNT.defaultPrice + incrementPrice;
+    }
+    return 0;
+  }
+
+  static getWeekdayDiscount(visitDate, count) {
+    if (visitDate.isWeekday()) {
+      return WEEKDAY_DISCOUNT.price * count;
+    }
+    return 0;
+  }
+
+  static getWeekendDiscount(visitDate, count) {
+    if (visitDate.isWeekend()) {
+      return WEEKEND_DISCOUNT.price * count;
+    }
+    return 0;
+  }
+
+  static getSpecialDiscount(visitDate) {
+    if (visitDate.isSpecialDay()) {
+      return SPECIAL_DISCOUNT.price;
+    }
+    return 0;
+  }
+
+  static getGiftDiscount(totalPrice) {
+    if (totalPrice >= GIFT_DISCOUNT.triggerPrice) {
+      return GIFT_DISCOUNT.price * GIFT_DISCOUNT.count;
+    }
+    return 0;
+  }
+}
+
+export default Discount;

리뷰: 단순한 숫자 0인지 0원인지 의미가 모호해서 0도 상수처리 하면 더 가독성이 좋을 것 같습니다!
@@ -0,0 +1,49 @@
+import {
+  CHRISTMAS_DISCOUNT,
+  WEEKDAY_DISCOUNT,
+  WEEKEND_DISCOUNT,
+  SPECIAL_DISCOUNT,
+  GIFT_DISCOUNT,
+} from '../utils/constants/discount';
+
+class Discount {
+  static getChristmasDiscount(visitDate) {
+    if (visitDate.isChristmasDay()) {
+      const date = visitDate.getVisitDate();
+      const incrementPrice = (date - CHRISTMAS_DISCOUNT.startDate) * CHRISTMAS_DISCOUNT.incrementPrice;
+
+      return CHRISTMAS_DISCOUNT.defaultPrice + incrementPrice;
+    }
+    return 0;
+  }
+
+  static getWeekdayDiscount(visitDate, count) {
+    if (visitDate.isWeekday()) {
+      return WEEKDAY_DISCOUNT.price * count;
+    }
+    return 0;
+  }
+
+  static getWeekendDiscount(visitDate, count) {
+    if (visitDate.isWeekend()) {
+      return WEEKEND_DISCOUNT.price * count;
+    }
+    return 0;
+  }
+
+  static getSpecialDiscount(visitDate) {
+    if (visitDate.isSpecialDay()) {
+      return SPECIAL_DISCOUNT.price;
+    }
+    return 0;
+  }
+
+  static getGiftDiscount(totalPrice) {
+    if (totalPrice >= GIFT_DISCOUNT.triggerPrice) {
+      return GIFT_DISCOUNT.price * GIFT_DISCOUNT.count;
+    }
+    return 0;
+  }
+}
+
+export default Discount;

리뷰: 클래스명이 Discount 라면
Discount.getWeekdayDiscount 하게 되면 Discount 단어가 겹쳐서 불필요 하다고 생각합니다. 단순히 getWeekday 만 해줘도 값을 불러올때 Discount.getWeekday 가 되니 의미를
부여하기에 충분하다고 생각합니다. 저같은 경우 Event 라는 클래스에 해당 기능이 있어서 weekdayDiscount 라 지었지만 클래스명이 Discount 라면 weekday만 해도 충분할 것 같습니다!
@@ -0,0 +1,34 @@
+import { MENU } from '../utils/constants/menu';
+import { validateMenuName } from '../utils/validators/validateMenu';
+
+class Menu {
+  #name;
+  #price;
+  #type;
+
+  constructor(name) {
+    validateMenuName(name);
+    this.#name = name;
+
+    Object.entries(MENU).forEach(([type, menus]) => {
+      if (menus.hasOwnProperty(name)) {
+        this.#price = menus[name];
+        this.#type = type;
+      }
+    });
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  getName() {
+    return this.#name;
+  }
+
+  getType() {
+    return this.#type;
+  }
+}
+
+export default Menu;

리뷰: 3주차 공통 피드백을 보면 필드의 개수가 3개 이상이면 많다고 판단하는 것 같습니다.
name 과 price는 꼭 필요하지만 type 같은 경우 메서드 내에서 로직처리해서 반환하는 구조로 가도 괜찮을 것 같습니다.
@@ -0,0 +1,47 @@
+import Menu from './Menu';
+import { WEEKDAY_DISCOUNT, WEEKEND_DISCOUNT } from '../utils/constants/discount';
+import { DELIMITER } from '../utils/constants/event';
+import { validateDuplicateOrder, validateOnlyDrink, validateTotalMenuCount } from '../utils/validators/vaildateOrder';
+import { validateMenuCount } from '../utils/validators/validateMenu';
+
+class Order {
+  #order;
+
+  constructor(orderInput) {
+    this.#order = new Map();
+
+    orderInput.split(DELIMITER.orderDelimiter).forEach((menu) => {
+      this.#addMenu(menu);
+    });
+
+    validateOnlyDrink(this.#order);
+    validateTotalMenuCount(this.#order);
+  }
+
+  #addMenu(menu) {
+    const [name, count] = menu.split(DELIMITER.menuDelimiter);
+
+    if (validateDuplicateOrder(name, this.#order)) {
+      validateMenuCount(Number(count));
+      this.#order.set(new Menu(name), Number(count));
+    }
+  }
+
+  getWeekdayMenuCount() {
+    return Array.from(this.#order.entries())
+      .filter(([menu]) => WEEKDAY_DISCOUNT.menuType === menu.getType())
+      .reduce((totalCount, [_, count]) => totalCount + count, 0);
+  }
+
+  getWeekendMenuCount() {
+    return Array.from(this.#order.entries())
+      .filter(([menu]) => WEEKEND_DISCOUNT.menuType === menu.getType())
+      .reduce((totalCount, [_, count]) => totalCount + count, 0);
+  }
+
+  getOrder() {
+    return this.#order;
+  }
+}
+
+export default Order;

리뷰: desert 와 main 메뉴 개수를 계산하는 로직을 따로 분리하기 보다 음식의 타입을 넣으면 해당 타입의 음식 개수를 반환하도록  짜보는건 어떨까요? 평일에는 디저트 메뉴를 할인, 주말에는 메인 메뉴를 할인 한다고 되어있지만 만약 비즈니스 팀 회의 결과 평일에는 디저트가 아닌 에피타이저를 할인하는걸로 해주세요! 라고 말이 바뀌는 것에 대비해서 처음 설계 단계부터 확장성있는 코드를 짠다면 이후 보수에 더 용이할 것 같습니다
@@ -0,0 +1,59 @@
+import Discount from './Discount';
+import Order from './Order';
+import VisitDate from './VisitDate';
+import { BADGE, EVENT } from '../utils/constants/event';
+
+class Planner {
+  #visitDate;
+  #order;
+  #price;
+
+  setVisitDate(visitDate) {
+    this.#visitDate = new VisitDate(visitDate);
+  }
+
+  setOrder(order) {
+    this.#order = new Order(order);
+    this.#price = Array.from(this.#order.getOrder().entries()).reduce(
+      (price, [menu, count]) => price + menu.getPrice() * count,
+      0,
+    );
+  }
+
+  getVisitDate() {
+    return this.#visitDate.getVisitDate();
+  }
+
+  getOrder() {
+    return this.#order.getOrder();
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  caculateDiscount() {
+    if (this.#price < EVENT.discountTriggerPrice) {
+      return [{}, 0];
+    }
+    const discount = {
+      christmas: Discount.getChristmasDiscount(this.#visitDate),
+      weekday: Discount.getWeekdayDiscount(this.#visitDate, this.#order.getWeekdayMenuCount()),
+      weekend: Discount.getWeekendDiscount(this.#visitDate, this.#order.getWeekendMenuCount()),
+      special: Discount.getSpecialDiscount(this.#visitDate),
+      gift: Discount.getGiftDiscount(this.#price),
+    };
+    const discountPrice = Object.values(discount).reduce((total, discount) => total + discount, 0);
+    this.#price -= discountPrice - discount.gift;
+
+    return [discount, discountPrice];
+  }
+
+  getBadge(discountPrice) {
+    const badge = Object.values(BADGE).find(({ price }) => discountPrice >= price);
+
+    return badge?.name;
+  }
+}
+
+export default Planner;

리뷰: 이번 미션에서 단순히 price라는 단어는 의미가 다소 광범위 하다고 생각합니다. price는 주문 총액이 될 수도 있고, 혜택 금액, 할인 후 최종 결제 금액도 될 수 있기 때문입니다. 또한 visitDate 와 order 는 클래스를 선언하기에 꼭 필요하지만 price는 메서드 내에서 처리하여 필드수를 줄여도 괜찮을 것 같습니다. 현재 로직에선 필드로 빼는게 유리해 보이지만, 처음 로직을 짤때 필드수를 줄이는 방향으로 고민해보는것도 좋을 것 같습니다. 서윤님이라면 충분히 하실 수 있을것 같아 말씀드립니다
@@ -0,0 +1,24 @@
+import { EVENT, VALIDATION } from '../constants/event';
+
+const visitDate = (detail) => `유효하지 않은 날짜입니다. 다시 입력해 주세요. ${detail}`;
+const order = (detail) => `유효하지 않은 주문입니다. 다시 입력해 주세요. ${detail}`;
+
+export const VISIT_DATE_MESSAGE = Object.freeze({
+  invalidNumber: visitDate('날짜는 숫자여야 합니다.'),
+  invalidRange: visitDate(`12월 ${EVENT.startDate}일부터 ${EVENT.endDate}일까지만 입력 가능합니다.`),
+});
+
+export const ORDER_MESSAGE = Object.freeze({
+  duplicateMenu: order('주문에 중복된 메뉴가 존재합니다.'),
+  onlyDrinkMenu: order(`주문은 음료만 주문할 수 없습니다.`),
+  maxOrderCount: order(`메뉴는 총 ${VALIDATION.maxMenuCount}개 이하로 주문해야 합니다.`),
+});
+
+export const MENU_MESSAGE = Object.freeze({
+  isNotExistMenu: order('존재하지 않는 메뉴입니다.'),
+});
+
+export const MENU_COUNT_MESSAGE = Object.freeze({
+  invalidNumber: order('메뉴 개수는 숫자여야 합니다.'),
+  minMenuCount: order(`메뉴는 ${VALIDATION.minMenuCount}개 이상으로 주문해야 합니다.`),
+});

리뷰: 저도 각각의 상황에 맞는 에러 메세지를 띄우고자 하였지만 비즈니스팀의 요청사항에 
메뉴형식, 메뉴중복, 메뉴개수,메뉴판에 없는 메뉴 들에 대한 오류는 모두 "유효하지 않는 주문입니다."로 통일해 달라고 하였습니다.혹시 최종 코테에 가시게 된다면 같은 이유로 놓치지 않을까 싶어서 말씀드립니다ㅠㅠ
@@ -0,0 +1,56 @@
+import Planner from '../domain/Planner';
+import InputView from '../view/InputView';
+import OutputView from '../view/OutputView';
+
+class ChristmasController {
+  #planner;
+
+  constructor() {
+    this.#planner = new Planner();
+  }
+
+  async play() {
+    await this.#setPlan();
+    this.#showBeforeDiscount();
+    this.#showAfterDiscount();
+  }
+
+  async #setPlan() {
+    await this.#retryInput(async () => {
+      const visitDate = await InputView.readVisitDate();
+      this.#planner.setVisitDate(visitDate);
+    });
+
+    await this.#retryInput(async () => {
+      const order = await InputView.readOrder();
+      this.#planner.setOrder(order);
+    });
+  }
+
+  #showBeforeDiscount() {
+    OutputView.printPreviewTitle(this.#planner.getVisitDate());
+    OutputView.printOrder(this.#planner.getOrder());
+    OutputView.printPriceBeforeDiscount(this.#planner.getPrice());
+  }
+
+  #showAfterDiscount() {
+    const [discount, discountTotal] = this.#planner.caculateDiscount();
+
+    OutputView.printGift(discount?.gift);
+    OutputView.printDiscountInfo(discount);
+    OutputView.printTotalDiscount(discountTotal);
+    OutputView.printPriceAfterDiscount(this.#planner.getPrice());
+    OutputView.printBadge(this.#planner.getBadge(discountTotal));
+  }
+
+  async #retryInput(func) {
+    try {
+      return await func();
+    } catch (error) {
+      OutputView.printError(error.message);
+      return this.#retryInput(func);
+    }
+  }
+}
+
+export default ChristmasController;

리뷰: controller 라는 디렉토리 내부에 있는 파일들을 controller 의미를 내포하고 있다고 생각합니다.
ChristmasController 대신 음.. 저도 딱히 생각나는게 없네요 ㅠㅠ
@@ -0,0 +1,34 @@
+import { MENU } from '../utils/constants/menu';
+import { validateMenuName } from '../utils/validators/validateMenu';
+
+class Menu {
+  #name;
+  #price;
+  #type;
+
+  constructor(name) {
+    validateMenuName(name);
+    this.#name = name;
+
+    Object.entries(MENU).forEach(([type, menus]) => {
+      if (menus.hasOwnProperty(name)) {
+        this.#price = menus[name];
+        this.#type = type;
+      }
+    });
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  getName() {
+    return this.#name;
+  }
+
+  getType() {
+    return this.#type;
+  }
+}
+
+export default Menu;

리뷰: 헉 저런 룰이 있었군요! 감사합니다!!
@@ -0,0 +1,34 @@
+import { MENU } from '../utils/constants/menu';
+import { validateMenuName } from '../utils/validators/validateMenu';
+
+class Menu {
+  #name;
+  #price;
+  #type;
+
+  constructor(name) {
+    validateMenuName(name);
+    this.#name = name;
+
+    Object.entries(MENU).forEach(([type, menus]) => {
+      if (menus.hasOwnProperty(name)) {
+        this.#price = menus[name];
+        this.#type = type;
+      }
+    });
+  }
+
+  getPrice() {
+    return this.#price;
+  }
+
+  getName() {
+    return this.#name;
+  }
+
+  getType() {
+    return this.#type;
+  }
+}
+
+export default Menu;

리뷰: 공통피드백에서 3개를 많다고 판단하기보단, 불필요한 필드는 없애자고 하는 것 같더라구요!
말씀하신 것처럼 type에 대해 많이 고민했었는데, 매번 constant에 있는 메뉴판 배열을 돌면서 어떤 타입인지 판단하는 것 보다는, 그냥 처음부터 속성으로 넣자! 라고 생각했었습니다!
type을 뺐을때 어떻게 깔끔하게 처리할 수 있을지 한번 고민해보겠습니다!
@@ -0,0 +1,4 @@
+export const priceFormatter = new Intl.NumberFormat('ko-KR', {
+  minimumFractionDigits: 0,
+  maximumFractionDigits: 0,
+});

리뷰: minimumFractionDigits: 0 소수점 이하 최소 자릿수를 0,
maximumFractionDigits: 0 소수점 이하 최대 자릿수를 0

이렇게 하면 12345.6789 =>12,346로 변환해줄 수 있습니다!