diff --git a/docs/README.md b/docs/README.md index e69de29..993de7a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,138 @@ +# 구현할 기능 목록 + +- [X] 방문날짜를 입력받고 생성한다. + - [X] 1 이상 31 이하 여야 한다. + - [X] 아닌 경우 IllegalArgumentException 발생시키고 입력 다시 받음 + - [X] 예외 메세지 : "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요." + + +- [X] 주문 메뉴와 개수를 입력받고 해당 객체를 생성한다. + - [X] 예외 상황 - 예외 메세지 출력 "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." + - [X] 고객이 메뉴판에 없는 메뉴를 입력하는 경우 + - [X] 메뉴의 개수는 1 이상의 숫자만 입력되도록 한다. + - [X] 메뉴 형식이 예시와 다른 경우 + - [X] 중복 메뉴를 입력한 경우(e.g. 시저샐러드-1,시저샐러드-1) + - [X] 음료만 주문하는 경우 + - [X] 메뉴 개수 총합 : 최대 20개 까지 가능. + + +- [X] 주문 메뉴 출력 + + +- [X] 이벤트 적용 + - [X] 총주문 금액 10000원 이상부터 적용 + + - [X] 이벤트 적용 기간 + - 크리스마스 디데이 할인 12/1 ~ 12/25 + - 이외 이벤트 12/1 ~ 12/31 + + - [X] 크리스마스 디데이 할인 + - 1,000원으로 시작하여 크리스마스가 다가올수록 날마다 할인 금액이 100원씩 증가 + - 총주문 금액에서 해당 금액만큼 할인 + (e.g. 시작일인 12월 1일에 1,000원, 2일에 1,100원, ..., 25일엔 3,400원 할인) + + - [X] 평일 할인 + - 일~목 / 디저트 메뉴를 메뉴 1개당 2,023원 할인 + + - [X] 주말 할인 + - 금,토 / 메인 메뉴를 메뉴 1개당 2,023원 할인 + + - [X] 특별 할인 + - 3, 10, 17, 24, 25, 31 + - 총주문 금액 에서 1,000원 할인 + + - [X] 증정 이벤트 + - 할인 전 총주문 금액이 12만 원 이상일 때, 샴페인 1개 증정 + + +- [X] 배지 계산 + - [X] 총혜택금액 기준 + - 산타 : 5_000원 이상 + - 트리 : 10_000원 이상 + - 별 : 20_000원 이상 + + +- [X] 아래 내용 출력 + - [X] 할인 전 총주문 금액 + - 형식 : "8,500원" + - [X] 증정 메뉴 + - "없음" || "샴페인 1개" + - [X] 혜택 내역 + - 형식 : "없음" || "크리스마스 디데이 할인: -1,200원" + - [X] 총혜택 금액 = 할인 금액의 합계 + 증정 메뉴의 가격 + - 형식 : "0원" || "-31,246원" + - [X] 할인 후 예상 결제 금액 = 할인 전 총주문 금액 - 할인 금액 + - 형식 : "8,500원" + - [X] 배지 출력 + - 형식 : "없음" || "산타" + + + +--- +### 전체 흐름 +``` +안녕하세요! 우테코 식당 12월 이벤트 플래너입니다. +12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!) +26 +주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1) +타파스-1,제로콜라-1 +12월 26일에 우테코 식당에서 받을 이벤트 혜택 미리 보기! + +<주문 메뉴> +타파스 1개 +제로콜라 1개 + +<할인 전 총주문 금액> +8,500원 + +<증정 메뉴> +없음 + +<혜택 내역> +없음 + +<총혜택 금액> +0원 + +<할인 후 예상 결제 금액> +8,500원 + +<12월 이벤트 배지> +없음 +``` + +``` +안녕하세요! 우테코 식당 12월 이벤트 플래너입니다. +12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!) +3 +주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1) +티본스테이크-1,바비큐립-1,초코케이크-2,제로콜라-1 +12월 3일에 우테코 식당에서 받을 이벤트 혜택 미리 보기! + +<주문 메뉴> +티본스테이크 1개 +바비큐립 1개 +초코케이크 2개 +제로콜라 1개 + +<할인 전 총주문 금액> +142,000원 + +<증정 메뉴> +샴페인 1개 + +<혜택 내역> +크리스마스 디데이 할인: -1,200원 +평일 할인: -4,046원 +특별 할인: -1,000원 +증정 이벤트: -25,000원 + +<총혜택 금액> +-31,246원 + +<할인 후 예상 결제 금액> +135,754원 + +<12월 이벤트 배지> +산타 +``` \ No newline at end of file diff --git a/src/main/java/christmas/Application.java b/src/main/java/christmas/Application.java index b9ba6a2..5f814e2 100644 --- a/src/main/java/christmas/Application.java +++ b/src/main/java/christmas/Application.java @@ -1,7 +1,12 @@ package christmas; +import camp.nextstep.edu.missionutils.Console; +import christmas.controller.MainController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + MainController mainController = MainController.create(); + mainController.run(); + Console.close(); } } diff --git a/src/main/java/christmas/constants/BenefitConstants.java b/src/main/java/christmas/constants/BenefitConstants.java new file mode 100644 index 0000000..7770716 --- /dev/null +++ b/src/main/java/christmas/constants/BenefitConstants.java @@ -0,0 +1,16 @@ +package christmas.constants; + +import christmas.domain.menu.Menu; + +public class BenefitConstants { + public static final long ORDER_MINIMUM = 10000; + public static final long GIVE_AWAY_MINIMUM = 120000; + public static final long CHRISTMAS_D_DAY_BASE_BENEFIT = 1000; + public static final long CHRISTMAS_D_DAY_DAILY_BENEFIT = 100; + public static final long WEEKDAY_BENEFIT_UNIT = 2023; + public static final long WEEKEND_BENEFIT_UNIT = 2023; + public static final long SPECIAL_BENEFIT = 1000; + public static final Menu GIVE_AWAY_PRODUCT = Menu.샴페인; + public static final String GIVE_AWAY_PRODUCT_NAME = Menu.샴페인.name(); + public static final long GIVE_AWAY_BENEFIT_AMOUNT = GIVE_AWAY_PRODUCT.getPrice(); +} diff --git a/src/main/java/christmas/controller/MainController.java b/src/main/java/christmas/controller/MainController.java new file mode 100644 index 0000000..75708e0 --- /dev/null +++ b/src/main/java/christmas/controller/MainController.java @@ -0,0 +1,66 @@ +package christmas.controller; + +import christmas.domain.EventFinder; +import christmas.domain.event.MatchingEvents; +import christmas.dto.*; +import christmas.domain.orders.OrderItem; +import christmas.domain.orders.Orders; +import christmas.domain.visitingDate.VisitingDate; +import christmas.view.InputView; +import christmas.view.OutputView; + +import java.util.List; +import java.util.function.Supplier; + +public class MainController { + private final InputView inputView; + private final OutputView outputView; + + private MainController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public static MainController create() { + return new MainController(InputView.getInstance(), OutputView.getInstance()); + } + + public void run() { + outputView.printStart(); + VisitingDate date = createVisitingDate(); + Orders orders = createOrders(); + outputView.printResultStart(); + OrdersDto ordersDto = OrdersDto.from(orders); + outputView.printOrderDetail(ordersDto); + MatchingEvents matchingEvents = EventFinder.findMatchingEvents(date, orders); + ResultDto resultDto = ResultDto.of(orders, matchingEvents); + outputView.printResult(resultDto); + } + + private VisitingDate createVisitingDate() { + return readUserInput(() -> { + int input = inputView.readVisitingDate(); + return VisitingDate.from(input); + }); + } + + private Orders createOrders() { + return readUserInput(() -> { + List orderItemDtos = inputView.readOrderItemDtos(); + List orderItems = orderItemDtos.stream() + .map(dto -> OrderItem.of(dto.getName(), dto.getQuantity())) + .toList(); + return Orders.from(orderItems); + }); + } + + private T readUserInput(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + outputView.printError(e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/christmas/domain/Badge.java b/src/main/java/christmas/domain/Badge.java new file mode 100644 index 0000000..59e4ff2 --- /dev/null +++ b/src/main/java/christmas/domain/Badge.java @@ -0,0 +1,40 @@ +package christmas.domain; + +import java.util.Arrays; + +public enum Badge { + NONE(null, 0L, 5_000), + STAR("별", 5_000, 10_000), + TREE("트리", 10_000, 20_000), + SANTA("산타", 20_000, Long.MAX_VALUE); + + private final String badgeName; + private final long minimumBenefitAmount; + private final long maximumBenefitAmount; + + Badge(String badgeName, long minimumBenefitAmount, long maximumBenefitAmount) { + this.badgeName = badgeName; + this.minimumBenefitAmount = minimumBenefitAmount; + this.maximumBenefitAmount = maximumBenefitAmount; + } + + public static Badge findBadgeByCondition(long totalBenefitAmount) { + return Arrays.stream(Badge.values()) + .filter(badge -> totalBenefitAmount >= badge.getMinimumBenefitAmount() + && totalBenefitAmount < badge.getMaximumBenefitAmount()) + .findFirst() + .get(); + } + + public long getMinimumBenefitAmount() { + return minimumBenefitAmount; + } + + public long getMaximumBenefitAmount() { + return maximumBenefitAmount; + } + + public String getBadgeName() { + return badgeName; + } +} diff --git a/src/main/java/christmas/domain/EventFinder.java b/src/main/java/christmas/domain/EventFinder.java new file mode 100644 index 0000000..5d8423e --- /dev/null +++ b/src/main/java/christmas/domain/EventFinder.java @@ -0,0 +1,21 @@ +package christmas.domain; + +import christmas.domain.event.EventDetail; +import christmas.domain.event.MatchingEvent; +import christmas.domain.event.MatchingEvents; +import christmas.domain.orders.Orders; +import christmas.domain.visitingDate.VisitingDate; + +import java.util.List; + +public class EventFinder { + private EventFinder() { + } + public static MatchingEvents findMatchingEvents(VisitingDate date, Orders orders) { + List events = EventDetail.findEventByCondition(date, orders); + List matchingEvents = events.stream() + .map(event -> MatchingEvent.of(event, event.calculateBenefitAmount(date, orders))) + .toList(); + return MatchingEvents.from(matchingEvents); + } +} \ No newline at end of file diff --git a/src/main/java/christmas/domain/event/EventDetail.java b/src/main/java/christmas/domain/event/EventDetail.java new file mode 100644 index 0000000..9366814 --- /dev/null +++ b/src/main/java/christmas/domain/event/EventDetail.java @@ -0,0 +1,72 @@ +package christmas.domain.event; + +import christmas.domain.orders.Orders; +import christmas.domain.visitingDate.VisitingDate; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +import static christmas.constants.BenefitConstants.*; +import static christmas.domain.menu.MenuType.DESSERT; +import static christmas.domain.menu.MenuType.MAIN; + +public enum EventDetail { + NONE(null, + date -> true, + orders -> orders.calculateTotalPrice() < ORDER_MINIMUM, + (date, orders) -> 0L), + CHRISTMAS_D_DAY("크리스마스 디데이 할인", + VisitingDate::isBeforeChristmas, + orders -> orders.calculateTotalPrice() >= ORDER_MINIMUM, + (date, orders) -> CHRISTMAS_D_DAY_BASE_BENEFIT + + date.getChristmasDDayBenefitDate() * CHRISTMAS_D_DAY_DAILY_BENEFIT), + WEEKDAY("평일 할인", + VisitingDate::isWeekday, + orders -> orders.calculateTotalPrice() >= ORDER_MINIMUM && orders.containsMenuType(DESSERT), + (date, orders) -> orders.countMenuType(DESSERT) * WEEKDAY_BENEFIT_UNIT), + WEEKEND("주말 할인", + VisitingDate::isWeekend, + orders -> orders.calculateTotalPrice() >= ORDER_MINIMUM && orders.containsMenuType(MAIN), + (date, orders) -> orders.countMenuType(MAIN) * WEEKEND_BENEFIT_UNIT), + SPECIAL("특별 할인", + VisitingDate::isSpecial, + orders -> orders.calculateTotalPrice() >= ORDER_MINIMUM, + (date, orders) -> SPECIAL_BENEFIT), + GIVE_AWAY("증정 이벤트", + date -> true, + orders -> orders.calculateTotalPrice() >= GIVE_AWAY_MINIMUM, + (date, orders) -> GIVE_AWAY_BENEFIT_AMOUNT); + + private final String eventName; + private final Predicate dateCondition; + private final Predicate ordersCondition; + private final BiFunction benefitAmount; + + EventDetail(String eventName, Predicate dateCondition, Predicate ordersCondition, + BiFunction benefitAmount) { + this.eventName = eventName; + this.dateCondition = dateCondition; + this.ordersCondition = ordersCondition; + this.benefitAmount = benefitAmount; + } + + public static List findEventByCondition(VisitingDate date, Orders orders) { + return Arrays.stream(EventDetail.values()) + .filter(eventDetail -> eventDetail.dateCondition.test(date) && eventDetail.ordersCondition.test(orders)) + .toList(); + } + + public long calculateBenefitAmount(VisitingDate date, Orders orders) { + return benefitAmount.apply(date, orders); + } + + public boolean isEqual(EventDetail event) { + return this == event; + } + + public String getEventName() { + return eventName; + } +} \ No newline at end of file diff --git a/src/main/java/christmas/domain/event/MatchingEvent.java b/src/main/java/christmas/domain/event/MatchingEvent.java new file mode 100644 index 0000000..baefca0 --- /dev/null +++ b/src/main/java/christmas/domain/event/MatchingEvent.java @@ -0,0 +1,27 @@ +package christmas.domain.event; + +public class MatchingEvent { + private final EventDetail eventDetail; + private final long benefitAmount; + + private MatchingEvent(EventDetail eventDetail, long benefitAmount) { + this.eventDetail = eventDetail; + this.benefitAmount = benefitAmount; + } + + public static MatchingEvent of(EventDetail eventDetail, long benefitAmount) { + return new MatchingEvent(eventDetail, benefitAmount); + } + + public String getEventName() { + return eventDetail.getEventName(); + } + + public long getBenefitAmount() { + return benefitAmount; + } + + public boolean isEqualEvent(EventDetail event) { + return eventDetail.isEqual(event); + } +} diff --git a/src/main/java/christmas/domain/event/MatchingEvents.java b/src/main/java/christmas/domain/event/MatchingEvents.java new file mode 100644 index 0000000..8a04a56 --- /dev/null +++ b/src/main/java/christmas/domain/event/MatchingEvents.java @@ -0,0 +1,44 @@ +package christmas.domain.event; + + +import christmas.domain.Badge; + +import java.util.List; + +import static christmas.domain.event.EventDetail.GIVE_AWAY; + +public class MatchingEvents { + private final List matchingEvents; + + private MatchingEvents(List matchingEvents) { + this.matchingEvents = matchingEvents; + } + + public static MatchingEvents from(List events) { + return new MatchingEvents(events); + } + + public List getEvents() { + return List.copyOf(matchingEvents); + } + + public boolean containsGiveAway() { + return matchingEvents.stream() + .anyMatch(event -> event.isEqualEvent(GIVE_AWAY)); + } + + public String findBadgeName() { + return findBadge().getBadgeName(); + } + + private Badge findBadge() { + long totalBenefitAmount = calculateTotalBenefitAmount(); + return Badge.findBadgeByCondition(totalBenefitAmount); + } + + public long calculateTotalBenefitAmount() { + return matchingEvents.stream() + .mapToLong(MatchingEvent::getBenefitAmount) + .sum(); + } +} diff --git a/src/main/java/christmas/domain/menu/Menu.java b/src/main/java/christmas/domain/menu/Menu.java new file mode 100644 index 0000000..90ecf42 --- /dev/null +++ b/src/main/java/christmas/domain/menu/Menu.java @@ -0,0 +1,48 @@ +package christmas.domain.menu; + +import java.util.Arrays; + +import static christmas.domain.menu.MenuType.*; +import static christmas.exception.ErrorMessage.INVALID_ORDER; + +public enum Menu { + 양송이수프(APPETIZER, 6_000), + 타파스(APPETIZER, 5_500), + 시저샐러드(APPETIZER, 8_000), + 티본스테이크(MAIN, 55_000), + 바비큐립(MAIN, 54_000), + 해산물파스타(MAIN, 35_000), + 크리스마스파스타(MAIN, 25_000), + 초코케이크(DESSERT, 15_000), + 아이스크림(DESSERT, 5_000), + 제로콜라(DRINK, 3_000), + 레드와인(DRINK, 60_000), + 샴페인(DRINK, 25_000); + + private final MenuType type; + private final long price; + + Menu(MenuType type, long price) { + this.type = type; + this.price = price; + } + + public static Menu findMenuByName(String name) { + return Arrays.stream(Menu.values()) + .filter(menu -> menu.isEqualName(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_ORDER.getMessage())); + } + + private boolean isEqualName(String name) { + return this.name().equals(name); + } + + public boolean isMenuType(MenuType menuType) { + return type == menuType; + } + + public long getPrice() { + return price; + } +} diff --git a/src/main/java/christmas/domain/menu/MenuType.java b/src/main/java/christmas/domain/menu/MenuType.java new file mode 100644 index 0000000..6b364bc --- /dev/null +++ b/src/main/java/christmas/domain/menu/MenuType.java @@ -0,0 +1,5 @@ +package christmas.domain.menu; + +public enum MenuType { + APPETIZER, MAIN, DESSERT, DRINK; +} diff --git a/src/main/java/christmas/domain/orders/OrderItem.java b/src/main/java/christmas/domain/orders/OrderItem.java new file mode 100644 index 0000000..6dfcf27 --- /dev/null +++ b/src/main/java/christmas/domain/orders/OrderItem.java @@ -0,0 +1,42 @@ +package christmas.domain.orders; + +import christmas.domain.menu.Menu; +import christmas.domain.menu.MenuType; +import christmas.utils.OrderItemValidator; + +public class OrderItem { + private final Menu menu; + private final int quantity; + + private OrderItem(Menu menu, int quantity) { + this.menu = menu; + this.quantity = quantity; + } + + public static OrderItem of(String menuName, int quantity) { + OrderItemValidator.validatePositive(quantity); + OrderItemValidator.validateSize(quantity); + Menu menu = Menu.findMenuByName(menuName); + return new OrderItem(menu, quantity); + } + + public long calculatePrice() { + return menu.getPrice() * quantity; + } + + public boolean isMenuType(MenuType type) { + return menu.isMenuType(type); + } + + public Menu provideMenu() { + return menu; + } + + public String provideMenuName() { + return menu.name(); + } + + public int provideQuantity() { + return quantity; + } +} diff --git a/src/main/java/christmas/domain/orders/Orders.java b/src/main/java/christmas/domain/orders/Orders.java new file mode 100644 index 0000000..13e7177 --- /dev/null +++ b/src/main/java/christmas/domain/orders/Orders.java @@ -0,0 +1,43 @@ +package christmas.domain.orders; + +import christmas.domain.menu.MenuType; +import christmas.utils.OrdersValidator; + +import java.util.List; + +public class Orders { + private final List orderItems; + + private Orders(List orderItems) { + this.orderItems = orderItems; + } + + public static Orders from(List orderItems) { + OrdersValidator.validateDuplicate(orderItems); + OrdersValidator.validateOnlyDrink(orderItems); + OrdersValidator.validateSize(orderItems); + return new Orders(orderItems); + } + + public long calculateTotalPrice() { + return orderItems.stream() + .mapToLong(OrderItem::calculatePrice) + .sum(); + } + + public boolean containsMenuType(MenuType type) { + return orderItems.stream() + .anyMatch(item -> item.isMenuType(type)); + } + + public int countMenuType(MenuType type) { + return orderItems.stream() + .filter(orderItem -> orderItem.isMenuType(type)) + .mapToInt(OrderItem::provideQuantity) + .sum(); + } + + public List provideOrderItems() { + return orderItems; + } +} \ No newline at end of file diff --git a/src/main/java/christmas/domain/visitingDate/DateConstants.java b/src/main/java/christmas/domain/visitingDate/DateConstants.java new file mode 100644 index 0000000..9f83189 --- /dev/null +++ b/src/main/java/christmas/domain/visitingDate/DateConstants.java @@ -0,0 +1,19 @@ +package christmas.domain.visitingDate; + +public enum DateConstants { + DAYS_IN_WEEK(7), + CHRISTMAS_DAY(25), + WEEKEND_FIRST_DAY_MODULO(1), + WEEKEND_SECOND_DAY_MODULO(2), + SPECIAL_DAY_MODULO(3); + + private final int value; + + DateConstants(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/christmas/domain/visitingDate/VisitingDate.java b/src/main/java/christmas/domain/visitingDate/VisitingDate.java new file mode 100644 index 0000000..1f4be3d --- /dev/null +++ b/src/main/java/christmas/domain/visitingDate/VisitingDate.java @@ -0,0 +1,40 @@ +package christmas.domain.visitingDate; + + +import christmas.utils.VisitingDateValidator; + +import static christmas.domain.visitingDate.DateConstants.*; + +public class VisitingDate { + private final int date; + + private VisitingDate(int date) { + this.date = date; + } + + public static VisitingDate from(int date) { + VisitingDateValidator.validateDate(date); + return new VisitingDate(date); + } + + public boolean isBeforeChristmas() { + return date <= CHRISTMAS_DAY.getValue(); + } + + public boolean isSpecial() { + return date % DAYS_IN_WEEK.getValue() == SPECIAL_DAY_MODULO.getValue() || date == CHRISTMAS_DAY.getValue(); + } + + public boolean isWeekday() { + return !isWeekend(); + } + + public boolean isWeekend() { + return date % DAYS_IN_WEEK.getValue() == WEEKEND_FIRST_DAY_MODULO.getValue() + || date % DAYS_IN_WEEK.getValue() == WEEKEND_SECOND_DAY_MODULO.getValue(); + } + + public int getChristmasDDayBenefitDate() { + return date - 1; + } +} \ No newline at end of file diff --git a/src/main/java/christmas/dto/MatchingEventDto.java b/src/main/java/christmas/dto/MatchingEventDto.java new file mode 100644 index 0000000..cb8cd22 --- /dev/null +++ b/src/main/java/christmas/dto/MatchingEventDto.java @@ -0,0 +1,24 @@ +package christmas.dto; + + +public class MatchingEventDto { + private final String eventName; + private final long benefitAmount; + + private MatchingEventDto(String eventName, long benefitAmount) { + this.eventName = eventName; + this.benefitAmount = benefitAmount; + } + + public static MatchingEventDto of(String eventName, long benefitAmount) { + return new MatchingEventDto(eventName, benefitAmount); + } + + public String getEventName() { + return eventName; + } + + public long getBenefitAmount() { + return benefitAmount; + } +} \ No newline at end of file diff --git a/src/main/java/christmas/dto/MatchingEventsDto.java b/src/main/java/christmas/dto/MatchingEventsDto.java new file mode 100644 index 0000000..34cee70 --- /dev/null +++ b/src/main/java/christmas/dto/MatchingEventsDto.java @@ -0,0 +1,25 @@ +package christmas.dto; + + +import christmas.domain.event.MatchingEvents; + +import java.util.List; + +public class MatchingEventsDto { + private final List events; + + private MatchingEventsDto(List events) { + this.events = events; + } + + public static MatchingEventsDto from(MatchingEvents matchingEvents) { + List eventDtos = matchingEvents.getEvents().stream() + .map(event -> MatchingEventDto.of(event.getEventName(), event.getBenefitAmount())) + .toList(); + return new MatchingEventsDto(eventDtos); + } + + public List getEvents() { + return events; + } +} diff --git a/src/main/java/christmas/dto/OrderItemDto.java b/src/main/java/christmas/dto/OrderItemDto.java new file mode 100644 index 0000000..07f6689 --- /dev/null +++ b/src/main/java/christmas/dto/OrderItemDto.java @@ -0,0 +1,29 @@ +package christmas.dto; + +import christmas.domain.orders.OrderItem; + +public class OrderItemDto { + private final String name; + private final int quantity; + + private OrderItemDto(String name, int quantity) { + this.name = name; + this.quantity = quantity; + } + + public static OrderItemDto of(String name, int quantity) { + return new OrderItemDto(name, quantity); + } + + public static OrderItemDto from(OrderItem orderItem) { + return new OrderItemDto(orderItem.provideMenuName(), orderItem.provideQuantity()); + } + + public String getName() { + return name; + } + + public int getQuantity() { + return quantity; + } +} diff --git a/src/main/java/christmas/dto/OrdersDto.java b/src/main/java/christmas/dto/OrdersDto.java new file mode 100644 index 0000000..c51aa78 --- /dev/null +++ b/src/main/java/christmas/dto/OrdersDto.java @@ -0,0 +1,24 @@ +package christmas.dto; + +import christmas.domain.orders.Orders; + +import java.util.List; + +public class OrdersDto { + private final List orderItemDtos; + + private OrdersDto(List orderItemDtos) { + this.orderItemDtos = orderItemDtos; + } + + public static OrdersDto from(Orders orders) { + List orderItemDtos = orders.provideOrderItems().stream() + .map(OrderItemDto::from) + .toList(); + return new OrdersDto(orderItemDtos); + } + + public List getOrderItemDtos() { + return orderItemDtos; + } +} diff --git a/src/main/java/christmas/dto/ResultDto.java b/src/main/java/christmas/dto/ResultDto.java new file mode 100644 index 0000000..4ec0d72 --- /dev/null +++ b/src/main/java/christmas/dto/ResultDto.java @@ -0,0 +1,64 @@ +package christmas.dto; + +import christmas.domain.event.MatchingEvents; +import christmas.domain.orders.Orders; + +import static christmas.constants.BenefitConstants.GIVE_AWAY_BENEFIT_AMOUNT; + +public class ResultDto { + private final long originalAmount; + private final boolean containsGiveAway; + private final MatchingEventsDto matchingEventsDto; + private final long totalBenefitAmount; + private final long finalAmount; + private final String badgeName; + + private ResultDto(long originalAmount, boolean isGiveAway, MatchingEventsDto matchingEventsDto, + long totalBenefitAmount, long finalAmount, String badgeName) + { + this.originalAmount = originalAmount; + this.containsGiveAway = isGiveAway; + this.matchingEventsDto = matchingEventsDto; + this.totalBenefitAmount = totalBenefitAmount; + this.finalAmount = finalAmount; + this.badgeName = badgeName; + } + + public static ResultDto of(Orders orders, MatchingEvents matchingEvents) { + long originalAmount = orders.calculateTotalPrice(); + boolean containsGiveAway = matchingEvents.containsGiveAway(); + MatchingEventsDto matchingEventsDto = MatchingEventsDto.from(matchingEvents); + long totalBenefitAmount = matchingEvents.calculateTotalBenefitAmount(); + long discountAmount = totalBenefitAmount; + if (containsGiveAway) { + discountAmount = totalBenefitAmount - GIVE_AWAY_BENEFIT_AMOUNT; + } + long finalAmount = originalAmount - discountAmount; + String badgeName = matchingEvents.findBadgeName(); + return new ResultDto(originalAmount, containsGiveAway, matchingEventsDto, totalBenefitAmount, finalAmount, badgeName); + } + + public long getOriginalAmount() { + return originalAmount; + } + + public boolean isContainsGiveAway() { + return containsGiveAway; + } + + public MatchingEventsDto getMatchingEventsDto() { + return matchingEventsDto; + } + + public long getTotalBenefitAmount() { + return totalBenefitAmount; + } + + public long getFinalAmount() { + return finalAmount; + } + + public String getBadgeName() { + return badgeName; + } +} diff --git a/src/main/java/christmas/exception/ErrorMessage.java b/src/main/java/christmas/exception/ErrorMessage.java new file mode 100644 index 0000000..b4e66a4 --- /dev/null +++ b/src/main/java/christmas/exception/ErrorMessage.java @@ -0,0 +1,17 @@ +package christmas.exception; + +public enum ErrorMessage { + ERROR_CAPTION("[ERROR] "), + INVALID_VISITING_DATE("유효하지 않은 날짜입니다. 다시 입력해 주세요."), + INVALID_ORDER("유효하지 않은 주문입니다. 다시 입력해 주세요."); + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return ERROR_CAPTION.message + message; + } +} diff --git a/src/main/java/christmas/utils/OrderItemValidator.java b/src/main/java/christmas/utils/OrderItemValidator.java new file mode 100644 index 0000000..79624d1 --- /dev/null +++ b/src/main/java/christmas/utils/OrderItemValidator.java @@ -0,0 +1,18 @@ +package christmas.utils; + +import static christmas.exception.ErrorMessage.INVALID_ORDER; +import static christmas.utils.OrdersValidator.MAXIMUM_TOTAL_QUANTITY; + +public class OrderItemValidator { + public static void validatePositive(int value) { + if (value <= 0) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } + + public static void validateSize(int value) { + if (value > MAXIMUM_TOTAL_QUANTITY) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } +} diff --git a/src/main/java/christmas/utils/OrdersValidator.java b/src/main/java/christmas/utils/OrdersValidator.java new file mode 100644 index 0000000..220b2e8 --- /dev/null +++ b/src/main/java/christmas/utils/OrdersValidator.java @@ -0,0 +1,73 @@ +package christmas.utils; + +import christmas.domain.orders.OrderItem; +import org.junit.platform.commons.util.StringUtils; + +import java.util.List; + +import static christmas.domain.menu.MenuType.DRINK; +import static christmas.exception.ErrorMessage.INVALID_ORDER; + +public class OrdersValidator { + private static final int ORDER_PAIR_SIZE = 2; + public static final int MAXIMUM_TOTAL_QUANTITY = 20; + + public static List safeSplit(String input, String delimiter) { + validateEmpty(input); + validateStartsOrEndsWithDelimiter(input, delimiter); + return List.of(input.split(delimiter)); + } + + private static void validateEmpty(String input) { + if (StringUtils.isBlank(input)) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } + + private static void validateStartsOrEndsWithDelimiter(String input, String delimiter) { + if (input.startsWith(delimiter) || input.endsWith(delimiter)) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } + + public static int safeParseInt(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } + + public static void validatePair(List pair) { + if (pair.size() != ORDER_PAIR_SIZE) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } + + public static void validateDuplicate(List orderItems) { + long uniqueMenuCount = orderItems.stream() + .map(OrderItem::provideMenu) + .distinct() + .count(); + if (orderItems.size() != uniqueMenuCount) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } + + public static void validateOnlyDrink(List orderItems) { + boolean isOnlyDrink = orderItems.stream() + .allMatch(item -> item.isMenuType(DRINK)); + if (isOnlyDrink) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } + + public static void validateSize(List orderItems) { + int totalQuantity = orderItems.stream() + .mapToInt(OrderItem::provideQuantity) + .sum(); + if (totalQuantity > MAXIMUM_TOTAL_QUANTITY) { + throw new IllegalArgumentException(INVALID_ORDER.getMessage()); + } + } +} diff --git a/src/main/java/christmas/utils/VisitingDateValidator.java b/src/main/java/christmas/utils/VisitingDateValidator.java new file mode 100644 index 0000000..fa426fb --- /dev/null +++ b/src/main/java/christmas/utils/VisitingDateValidator.java @@ -0,0 +1,22 @@ +package christmas.utils; + +import static christmas.exception.ErrorMessage.INVALID_VISITING_DATE; + +public class VisitingDateValidator { + private static final int START_DATE = 1; + private static final int END_DATE = 31; + + public static int safeParseInt(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(INVALID_VISITING_DATE.getMessage()); + } + } + + public static void validateDate(int date) { + if (date < START_DATE || date > END_DATE) { + throw new IllegalArgumentException(INVALID_VISITING_DATE.getMessage()); + } + } +} diff --git a/src/main/java/christmas/view/InputView.java b/src/main/java/christmas/view/InputView.java new file mode 100644 index 0000000..767f0eb --- /dev/null +++ b/src/main/java/christmas/view/InputView.java @@ -0,0 +1,54 @@ +package christmas.view; + +import camp.nextstep.edu.missionutils.Console; +import christmas.dto.OrderItemDto; +import christmas.utils.OrdersValidator; +import christmas.utils.VisitingDateValidator; + +import java.util.List; +import java.util.stream.Collectors; + +public class InputView { + private static final InputView instance = new InputView(); + private static final String VISITING_DATE_MESSAGE = "12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)"; + private static final String ORDERS_MESSAGE = "주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)"; + private static final String ORDERS_DELIMITER = ","; + private static final String QUANTITY_DELIMITER = "-"; + private static final int ORDER_MENU_INDEX = 0; + private static final int ORDER_QUANTITY_INDEX = 1; + + + private InputView() { + } + + public static InputView getInstance() { + return instance; + } + + public int readVisitingDate() { + System.out.println(VISITING_DATE_MESSAGE); + String input = Console.readLine(); + return VisitingDateValidator.safeParseInt(input); + } + + public List readOrderItemDtos() { + System.out.println(ORDERS_MESSAGE); + String input = Console.readLine(); + List orders = OrdersValidator.safeSplit(input, ORDERS_DELIMITER); + return convertToOrderItemDtos(orders); + } + + private List convertToOrderItemDtos(List orders) { + return orders.stream() + .map(order -> OrdersValidator.safeSplit(order, QUANTITY_DELIMITER)) + .map(this::pairToOrderItemDto) + .collect(Collectors.toList()); + } + + private OrderItemDto pairToOrderItemDto(List pair) { + OrdersValidator.validatePair(pair); + String menuName = pair.get(ORDER_MENU_INDEX); + int quantity = OrdersValidator.safeParseInt(pair.get(ORDER_QUANTITY_INDEX)); + return OrderItemDto.of(menuName, quantity); + } +} \ No newline at end of file diff --git a/src/main/java/christmas/view/OutputView.java b/src/main/java/christmas/view/OutputView.java new file mode 100644 index 0000000..ae5c3ff --- /dev/null +++ b/src/main/java/christmas/view/OutputView.java @@ -0,0 +1,120 @@ +package christmas.view; + +import christmas.constants.BenefitConstants; +import christmas.dto.*; +import org.junit.platform.commons.util.StringUtils; + +public class OutputView { + private static final OutputView instance = new OutputView(); + private static final String START_MESSAGE = "안녕하세요! 우테코 식당 12월 이벤트 플래너입니다."; + private static final String RESULT_START_MESSAGE = "12월 26일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!"; + private static final String ORDER_MENU_TITLE = "<주문 메뉴>"; + private static final String ORDER_MENU_FORMAT = "%s %d개"; + private static final String ORIGINAL_AMOUNT_TITLE = "<할인 전 총주문 금액>"; + private static final String ORIGINAL_AMOUNT_FORMAT = "%,d원"; + private static final String GIVE_AWAY_TITLE = "<증정 메뉴>"; + private static final String GIVE_AWAY_FORMAT = "%s %d개"; + private static final String DEFAULT_MESSAGE = "없음"; + private static final String BENEFIT_TITLE = "<혜택 내역>"; + private static final String BENEFIT_FORMAT = "%s: -%,d원"; + private static final String BENEFIT_AMOUNT_TITLE = "<총혜택 금액>"; + private static final String BENEFIT_AMOUNT_FORMAT = "%,d원"; + private static final int BENEFIT_AMOUNT_UNIT = -1; + private static final String FINAL_AMOUNT_TITLE = "<할인 후 예상 결제 금액>"; + private static final String FINAL_AMOUNT_FORMAT = "%,d원"; + private static final String BADGE_TITLE = "<12월 이벤트 배지>"; + + private OutputView() { + } + + public static OutputView getInstance() { + return instance; + } + + public void printError(String errorMessage) { + System.out.println(errorMessage); + } + + public void printStart() { + System.out.println(START_MESSAGE); + } + + public void printResultStart() { + System.out.println(RESULT_START_MESSAGE); + } + + public void printOrderDetail(OrdersDto ordersDto) { + printLine(); + System.out.println(ORDER_MENU_TITLE); + ordersDto.getOrderItemDtos().forEach(this::printOrder); + } + + private static void printLine() { + System.out.println(); + } + + private void printOrder(OrderItemDto orderItemDto) { + System.out.printf((ORDER_MENU_FORMAT) + "%n", orderItemDto.getName(), orderItemDto.getQuantity()); + } + + public void printResult(ResultDto resultDto) { + printOriginalAmount(resultDto.getOriginalAmount()); + printGiveAway(resultDto.isContainsGiveAway()); + printMatchingEvents(resultDto.getMatchingEventsDto()); + printBenefitAmount(resultDto.getTotalBenefitAmount()); + printFinalAmount(resultDto.getFinalAmount()); + printBadgeName(resultDto.getBadgeName()); + } + + private void printOriginalAmount(long amount) { + printLine(); + System.out.println(ORIGINAL_AMOUNT_TITLE); + System.out.printf((ORIGINAL_AMOUNT_FORMAT) + "%n", amount); + } + + private void printGiveAway(boolean containsGiveAway) { + printLine(); + System.out.println(GIVE_AWAY_TITLE); + if (containsGiveAway) { + System.out.printf((GIVE_AWAY_FORMAT) + "%n", BenefitConstants.GIVE_AWAY_PRODUCT_NAME, 1); + return; + } + System.out.println(DEFAULT_MESSAGE); + } + + private void printMatchingEvents(MatchingEventsDto matchingEventsDto) { + printLine(); + System.out.println(BENEFIT_TITLE); + matchingEventsDto.getEvents().forEach(this::printMatchingEvent); + } + + private void printMatchingEvent(MatchingEventDto dto) { + if (StringUtils.isBlank(dto.getEventName())) { + System.out.println(DEFAULT_MESSAGE); + return; + } + System.out.printf((BENEFIT_FORMAT) + "%n", dto.getEventName(), dto.getBenefitAmount()); + } + + public void printBenefitAmount(long benefitAmount) { + printLine(); + System.out.println(BENEFIT_AMOUNT_TITLE); + System.out.printf((BENEFIT_AMOUNT_FORMAT) + "%n", benefitAmount * BENEFIT_AMOUNT_UNIT); + } + + private void printFinalAmount(long finalAmount) { + printLine(); + System.out.println(FINAL_AMOUNT_TITLE); + System.out.printf((FINAL_AMOUNT_FORMAT) + "%n", finalAmount); + } + + private void printBadgeName(String badgeName) { + printLine(); + System.out.println(BADGE_TITLE); + if (badgeName == null) { + System.out.println(DEFAULT_MESSAGE); + return; + } + System.out.println(badgeName); + } +} \ No newline at end of file diff --git a/src/test/java/christmas/domain/EventFinderTest.java b/src/test/java/christmas/domain/EventFinderTest.java new file mode 100644 index 0000000..73af3d5 --- /dev/null +++ b/src/test/java/christmas/domain/EventFinderTest.java @@ -0,0 +1,55 @@ +package christmas.domain; + +import christmas.domain.event.MatchingEvents; +import christmas.domain.menu.Menu; +import christmas.domain.orders.OrderItem; +import christmas.domain.orders.Orders; +import christmas.domain.visitingDate.VisitingDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + + +class EventFinderTest { + @DisplayName("생성 테스트") + @Test + void create() { + // given + int date1 = 10; //special, weekday, christmasDday, giveAway + int date2 = 19; //weekday, christmasDday, giveAway + int date3 = 22; //weekend, christmasDday, giveAway + int date4 = 31; //weekday, special, giveAway + + OrderItem appetizer = OrderItem.of(Menu.시저샐러드.name(), 1); + OrderItem main = OrderItem.of(Menu.바비큐립.name(), 2); + OrderItem dessert = OrderItem.of(Menu.아이스크림.name(), 3); + + Orders orders = Orders.from(List.of(appetizer, main, dessert)); + VisitingDate visitingDate1 = VisitingDate.from(date1); + VisitingDate visitingDate2 = VisitingDate.from(date2); + VisitingDate visitingDate3 = VisitingDate.from(date3); + VisitingDate visitingDate4 = VisitingDate.from(date4); + + // when + EventFinder reservation1 = EventFinder.of(visitingDate1, orders); + EventFinder reservation2 = EventFinder.of(visitingDate2, orders); + EventFinder reservation3 = EventFinder.of(visitingDate3, orders); + EventFinder reservation4 = EventFinder.of(visitingDate4, orders); + + MatchingEvents matchingEvents1 = reservation1.createMatchingEvents(); + MatchingEvents matchingEvents2 = reservation2.createMatchingEvents(); + MatchingEvents matchingEvents3 = reservation3.createMatchingEvents(); + MatchingEvents matchingEvents4 = reservation4.createMatchingEvents(); + + // then + assertThat(matchingEvents1.getEvents().size()).isEqualTo(4); + assertThat(matchingEvents2.getEvents().size()).isEqualTo(3); + assertThat(matchingEvents3.getEvents().size()).isEqualTo(3); + assertThat(matchingEvents4.getEvents().size()).isEqualTo(3); + + assertThat(matchingEvents1.calculateTotalBenefitAmount()).isEqualTo(33969L); + } +} \ No newline at end of file diff --git a/src/test/java/christmas/domain/orders/OrderItemTest.java b/src/test/java/christmas/domain/orders/OrderItemTest.java new file mode 100644 index 0000000..76fb4b3 --- /dev/null +++ b/src/test/java/christmas/domain/orders/OrderItemTest.java @@ -0,0 +1,37 @@ +package christmas.domain.orders; + +import christmas.domain.menu.Menu; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; + + +class OrderItemTest { + @DisplayName("OrderItem 정상 생성 테스트") + @Test + void create() { + // given, when + OrderItem orderItem = OrderItem.of(Menu.시저샐러드.name(), 1); + + // then + assertThat(orderItem).isNotNull(); + } + + @DisplayName("이름이 잘못된 경우 예외 발생한다.") + @Test + void exception_name() { + assertThatThrownBy(() -> OrderItem.of("invalidMenu", 1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest(name = "[{index}] 수량이 {0} 인 경우 예외 발생한다") + @ValueSource(ints = {-100, 0, 25}) + void exception_quantity(int element) { + assertThatThrownBy(() -> OrderItem.of(Menu.바비큐립.name(), element)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/christmas/domain/orders/OrdersTest.java b/src/test/java/christmas/domain/orders/OrdersTest.java new file mode 100644 index 0000000..770e0e9 --- /dev/null +++ b/src/test/java/christmas/domain/orders/OrdersTest.java @@ -0,0 +1,52 @@ +package christmas.domain.orders; + + +import christmas.domain.menu.Menu; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; + +class OrdersTest { + @DisplayName("Orders 정상 생성 테스트") + @Test + void create() { + // given + OrderItem item1 = OrderItem.of(Menu.시저샐러드.name(), 1); + OrderItem item2 = OrderItem.of(Menu.바비큐립.name(), 1); + OrderItem item3 = OrderItem.of(Menu.레드와인.name(), 2); + + //when + Orders orders = Orders.from(List.of(item1, item2, item3)); + + //then + assertThat(orders).isNotNull(); + } + + @ParameterizedTest(name = "[{index}] 예외가 발생한다.") + @MethodSource("orderItemsProvider") + void exception(List orderItems) { + assertThatThrownBy(() -> Orders.from(orderItems)) + .isInstanceOf(IllegalArgumentException.class); + } + + private static Stream orderItemsProvider() { + OrderItem item1 = OrderItem.of(Menu.레드와인.name(), 2); + OrderItem item2 = OrderItem.of(Menu.샴페인.name(), 2); + OrderItem item3 = OrderItem.of(Menu.바비큐립.name(), 10); + OrderItem item4 = OrderItem.of(Menu.샴페인.name(), 12); + + return Stream.of( + Arguments.of(List.of(item1, item2)), + Arguments.of(List.of(item3, item4)), + Arguments.of(List.of(item3, item3)) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/christmas/domain/visitingDate/VisitingDateTest.java b/src/test/java/christmas/domain/visitingDate/VisitingDateTest.java new file mode 100644 index 0000000..63fe404 --- /dev/null +++ b/src/test/java/christmas/domain/visitingDate/VisitingDateTest.java @@ -0,0 +1,25 @@ +package christmas.domain.visitingDate; + + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; + +class VisitingDateTest { + @DisplayName("VisitingDate 정상 생성 테스트") + @Test + void create() { + VisitingDate date = VisitingDate.from(1); + assertThat(date).isNotNull(); + } + + @ParameterizedTest(name = "[{index}] {0} 입력 시 예외 발생한다") + @ValueSource(ints = {-100, 0, 32, 100}) + void exception(int element) { + assertThatThrownBy(() -> VisitingDate.from(element)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file