diff --git a/kotlin-christmas/src/main/kotlin/christmas/Application.kt b/kotlin-christmas/src/main/kotlin/christmas/Application.kt index 8d101ce..bd62cdf 100644 --- a/kotlin-christmas/src/main/kotlin/christmas/Application.kt +++ b/kotlin-christmas/src/main/kotlin/christmas/Application.kt @@ -1,5 +1,14 @@ package christmas +import christmas.controller.ChristmasController +import christmas.service.ChristmasService +import christmas.view.InputView +import christmas.view.OutputView + fun main() { - TODO("프로그램 구현") + ChristmasController.run( + InputView, + OutputView, + ChristmasService + ) } diff --git a/kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt b/kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt new file mode 100644 index 0000000..0bf8727 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt @@ -0,0 +1,22 @@ +package christmas.badge + +import christmas.model.Price +import christmas.validation.ChristmasException + +enum class Badge(val minBenefitAmount: UInt) { + 별(5_000u), + 트리(10_000u), + 산타(20_000u), + ; + + companion object { + fun of(benefitAmount: Price) = + when (benefitAmount.value.toUInt()) { + in 0u until 별.minBenefitAmount -> null + in 별.minBenefitAmount until 트리.minBenefitAmount -> 별 + in 트리.minBenefitAmount until 산타.minBenefitAmount -> 트리 + in 산타.minBenefitAmount until UInt.MAX_VALUE -> 산타 + else -> throw ChristmasException("말도 안되는 혜택 금액입니다. 혜택 금액: $benefitAmount") + } + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/controller/ChristmasController.kt b/kotlin-christmas/src/main/kotlin/christmas/controller/ChristmasController.kt new file mode 100644 index 0000000..8d41e86 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/controller/ChristmasController.kt @@ -0,0 +1,18 @@ +package christmas.controller + +import christmas.service.ChristmasService +import christmas.view.InputView +import christmas.view.OutputView + +object ChristmasController { + fun run( + inputView: InputView, + outputView: OutputView, + christmasService: ChristmasService, + ) { + outputView.printWelcomeMessage() + val userOrderInfo = inputView.getUserOrderInfo() + val benefitInfo = christmasService.getBenefitInfo(userOrderInfo) + outputView.printUserEventBenefit(userOrderInfo, benefitInfo) + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt b/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt new file mode 100644 index 0000000..3494482 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt @@ -0,0 +1,109 @@ +package christmas.event + +import christmas.menu.Menu +import christmas.menu.MenuType +import christmas.menu.MenuWithCount +import christmas.model.Price +import christmas.model.UserOrderInfo +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.Month + +private const val MIN_EVENT_PRICE = 10_000 + +enum class EventPolicy { + GiveawayEventPolicy { + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = userOrderInfo.totalPrice >= 120_000 + + override fun getBenefit(userOrderInfo: UserOrderInfo): EventType = + EventType.Giveaway(MenuWithCount(Menu.샴페인, 1), this.getPolicyName()) + + override fun getPolicyName() = "증정 이벤트" + }, + + ChristmasDiscountPolicy { + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + userOrderInfo.estimatedVisitDate.isBefore(LocalDate.of(2023, 12, 26)) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount( + Price(1000 + 100 * (userOrderInfo.estimatedVisitDate.dayOfMonth - 1)), + this.getPolicyName() + ) + + override fun getPolicyName() = "크리스마스 디데이 할인" + }, + + WeekdayDiscountPolicy { + private val discountPricePerMenu = 2023 + private val availableDiscountDayOfWeek = listOf( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY + ) + + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + availableDiscountDayOfWeek.contains(userOrderInfo.estimatedVisitDate.dayOfWeek) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount( + Price(userOrderInfo.getSumPriceOfMenu(MenuType.Dessert) * discountPricePerMenu), + this.getPolicyName() + ) + + override fun getPolicyName() = "평일 할인" + }, + + WeekendDiscountPolicy { + private val discountPricePerMenu = 2023 + private val availableDiscountDayOfWeek = listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY) + + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + availableDiscountDayOfWeek.contains(userOrderInfo.estimatedVisitDate.dayOfWeek) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount( + Price(userOrderInfo.getSumPriceOfMenu(MenuType.Main) * discountPricePerMenu), + this.getPolicyName() + ) + + override fun getPolicyName() = "주말 할인" + }, + + SpecialDiscountPolicy { + private val discountPrice = 1000 + private val availableDiscountDay = listOf( + LocalDate.of(2023, 12, 3), + LocalDate.of(2023, 12, 10), + LocalDate.of(2023, 12, 17), + LocalDate.of(2023, 12, 24), + LocalDate.of(2023, 12, 25), + LocalDate.of(2023, 12, 31), + ) + + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + availableDiscountDay.contains(userOrderInfo.estimatedVisitDate) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount(Price(discountPrice), this.getPolicyName()) + + override fun getPolicyName() = "특별 할인" + }, + ; + + fun isSupported(userOrderInfo: UserOrderInfo) = + userOrderInfo.totalPrice >= MIN_EVENT_PRICE && + isInEventDate(userOrderInfo) && + isOwnSupported(userOrderInfo) + + private fun isInEventDate(userOrderInfo: UserOrderInfo) = + userOrderInfo.estimatedVisitDate.year == 2023 && userOrderInfo.estimatedVisitDate.month == Month.DECEMBER + + protected abstract fun isOwnSupported(userOrderInfo: UserOrderInfo): Boolean + + abstract fun getBenefit(userOrderInfo: UserOrderInfo): EventType + + protected abstract fun getPolicyName(): String +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt b/kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt new file mode 100644 index 0000000..2ddf7d6 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt @@ -0,0 +1,16 @@ +package christmas.event + +import christmas.menu.MenuWithCount +import christmas.model.Price + +sealed class EventType( + private val benefitAmount: Price, + private val eventPolicyName: String +) { + class Discount(val price: Price, eventPolicyName: String) : EventType(price, eventPolicyName) + + class Giveaway(val menuWithCount: MenuWithCount, eventPolicyName: String) : + EventType(menuWithCount.benefitAmount, eventPolicyName) + + override fun toString() = "${eventPolicyName}: ${benefitAmount.toMinusString()}" +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt b/kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt new file mode 100644 index 0000000..3cdf5f8 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt @@ -0,0 +1,34 @@ +package christmas.menu + +import christmas.model.Price + +enum class Menu(val price: Price, val menuType: MenuType) { + 양송이스프(Price(6_000), MenuType.Appetizer), + 타파스(Price(5_500), MenuType.Appetizer), + 시저샐러드(Price(8_000), MenuType.Appetizer), + 티본스테이크(Price(55_000), MenuType.Main), + 바비큐립(Price(54_000), MenuType.Main), + 해산물파스타(Price(35_000), MenuType.Main), + 크리스마스파스타(Price(25_000), MenuType.Main), + 초코케이크(Price(15_000), MenuType.Dessert), + 아이스크림(Price(5_000), MenuType.Dessert), + 제로콜라(Price(3_000), MenuType.Drink), + 레드와인(Price(60_000), MenuType.Drink), + 샴페인(Price(25_000), MenuType.Drink), + ; + +} + +enum class MenuType { + Appetizer, + Main, + Dessert, + Drink, + ; +} + +data class MenuWithCount(val menu: Menu, val count: Int) { + val benefitAmount = menu.price * count + + override fun toString() = "${this.menu.name} ${this.count}개" +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/model/Price.kt b/kotlin-christmas/src/main/kotlin/christmas/model/Price.kt new file mode 100644 index 0000000..6684944 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/model/Price.kt @@ -0,0 +1,27 @@ +package christmas.model + +import christmas.validation.requireChristmas +import java.text.DecimalFormat + +@JvmInline +value class Price(val value: Int) { + init { + requireChristmas(value >= 0) + } + + operator fun times(other: Int) = Price(this.value * other) + + operator fun plus(other: Price) = Price(this.value + other.value) + + operator fun minus(other: Price) = Price(this.value - other.value) + + operator fun compareTo(other: Price) = this.value.compareTo(other.value) + + operator fun compareTo(other: Int) = this.value.compareTo(other) + + fun toMinusString() = "${DecimalFormat("#,###").format(-value)}원" + + override fun toString() = "${DecimalFormat("#,###").format(value)}원" +} + +fun Int.toPrice() = Price(this) diff --git a/kotlin-christmas/src/main/kotlin/christmas/model/UserBenefitInfo.kt b/kotlin-christmas/src/main/kotlin/christmas/model/UserBenefitInfo.kt new file mode 100644 index 0000000..171160c --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/model/UserBenefitInfo.kt @@ -0,0 +1,25 @@ +package christmas.model + +import christmas.badge.Badge +import christmas.event.EventType + +class UserBenefitInfo( + val benefitList: List, +) { + val giveaway: EventType.Giveaway? = benefitList + .filterIsInstance().firstOrNull() + + val totalBenefitAmount: Price = benefitList.sumOf { + when (it) { + is EventType.Giveaway -> it.menuWithCount.menu.price.value * it.menuWithCount.count + is EventType.Discount -> it.price.value + } + }.toPrice() + + val totalDiscountAmount: Price = benefitList + .filterIsInstance() + .sumOf { it.price.value } + .toPrice() + + val eventBadge = Badge.of(totalBenefitAmount) +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt b/kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt new file mode 100644 index 0000000..6313dfc --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt @@ -0,0 +1,38 @@ +package christmas.model + +import christmas.menu.MenuType +import christmas.menu.MenuWithCount +import christmas.validation.requireChristmas +import java.time.LocalDate + +private const val MAX_AVAILABLE_MENU_COUNT = 20 + +class UserOrderInfo( + val userOrderMenu: UserOrderMenu, + estimatedVisitDay: VisitDay, +) { + val estimatedVisitDate: LocalDate = LocalDate.of(2023, 12, estimatedVisitDay.day) + val totalPrice: Price = userOrderMenu.getTotalPrice() + + fun getSumPriceOfMenu(menuType: MenuType) = + this.userOrderMenu.menus.sumOf { if (it.menu.menuType == menuType) it.count else 0 } + + @JvmInline + value class VisitDay(val day: Int) { + init { + requireChristmas(day in 1..31) { "유효하지 않은 날짜입니다. 다시 입력해 주세요." } + } + } + + @JvmInline + value class UserOrderMenu(val menus: List) { + init { + requireChristmas(menus.sumOf { it.count } <= MAX_AVAILABLE_MENU_COUNT) { "totalMenuCount는 ${MAX_AVAILABLE_MENU_COUNT}개를 넘을 수 없습니다." } + requireChristmas((menus.all { it.menu.menuType == MenuType.Drink }).not()) { "음료만 주문할 수 없습니다. 주문 메뉴: $menus" } + } + + fun getTotalPrice() = this.menus.sumOf { it.menu.price.value * it.count }.toPrice() + + override fun toString() = menus.joinToString("\n") + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/service/ChristmasService.kt b/kotlin-christmas/src/main/kotlin/christmas/service/ChristmasService.kt new file mode 100644 index 0000000..fa73536 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/service/ChristmasService.kt @@ -0,0 +1,22 @@ +package christmas.service + +import christmas.event.EventPolicy +import christmas.event.EventType +import christmas.model.UserBenefitInfo +import christmas.model.UserOrderInfo + +object ChristmasService { + + private val eventPolicyList = EventPolicy.entries + + fun getBenefitInfo(userOrderInfo: UserOrderInfo) = UserBenefitInfo(getBenefitList(userOrderInfo)) + + private fun getBenefitList(userOrderInfo: UserOrderInfo) = + mutableListOf().apply { + for (eventPolicy in eventPolicyList) { + if (eventPolicy.isSupported(userOrderInfo)) { + this.add(eventPolicy.getBenefit(userOrderInfo)) + } + } + }.toList() +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/validation/LottoValidation.kt b/kotlin-christmas/src/main/kotlin/christmas/validation/LottoValidation.kt new file mode 100644 index 0000000..7fba3c8 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/validation/LottoValidation.kt @@ -0,0 +1,17 @@ +package christmas.validation + +const val ERROR_PREFIX = "[ERROR] " + +fun requireChristmas(value: Boolean) { + requireChristmas(value) { "Failed requirement." } +} + +inline fun requireChristmas(value: Boolean, lazyMessage: () -> Any) { + if (!value) { + throw ChristmasException(lazyMessage().toString()) + } +} + +class ChristmasException(message: String) : IllegalArgumentException() { + override val message = ERROR_PREFIX + message +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/view/InputView.kt b/kotlin-christmas/src/main/kotlin/christmas/view/InputView.kt new file mode 100644 index 0000000..e4f0388 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/view/InputView.kt @@ -0,0 +1,61 @@ +package christmas.view + +import camp.nextstep.edu.missionutils.Console +import christmas.menu.Menu +import christmas.menu.MenuWithCount +import christmas.model.UserOrderInfo +import christmas.model.UserOrderInfo.UserOrderMenu +import christmas.model.UserOrderInfo.VisitDay +import christmas.validation.ChristmasException + +object InputView { + fun getUserOrderInfo(): UserOrderInfo { + val visitDay = tryGetUserInput({ println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)") }) + { Console.readLine().toVisitDay() } + val userOrderMenu = tryGetUserInput({ println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)") }) + { Console.readLine().toUserOrderMenu() } + return UserOrderInfo(userOrderMenu, visitDay) + } +} + +fun String.toVisitDay() = + try { + VisitDay(this.toInt()) + } catch (e: ChristmasException) { + throw e + } catch (e: Exception) { + throw ChristmasException("유효하지 않은 날짜입니다. 다시 입력해 주세요.") + } + +fun String.toUserOrderMenu() = + try { + val userOrderMenu = mutableListOf() + for (menuAndCount in this.split(",")) { + val (menuString, countString) = menuAndCount.split("-") + val menu = Menu.valueOf(menuString) + val count = countString.toInt() + val menuWithCount = MenuWithCount(menu, count) + if (userOrderMenu.contains(menuWithCount)) { + throw IllegalArgumentException() + } + userOrderMenu.add(menuWithCount) + } + UserOrderMenu(userOrderMenu) + } catch (e: ChristmasException) { + throw e + } catch (e: Exception) { + throw ChristmasException("유효하지 않은 주문입니다. 다시 입력해 주세요.") + } + +private fun tryGetUserInput(guideMessage: (() -> Unit)? = null, inputFunction: () -> T): T { + while (true) { + if (guideMessage != null) { + guideMessage() + } + try { + return inputFunction() + } catch (e: IllegalArgumentException) { + println(e.message) + } + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/view/OutputView.kt b/kotlin-christmas/src/main/kotlin/christmas/view/OutputView.kt new file mode 100644 index 0000000..162257e --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/view/OutputView.kt @@ -0,0 +1,42 @@ +package christmas.view + +import christmas.event.EventType +import christmas.model.UserBenefitInfo +import christmas.model.UserOrderInfo + +object OutputView { + fun printWelcomeMessage() { + println("안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.") + } + + fun printUserEventBenefit(userOrderInfo: UserOrderInfo, benefitInfo: UserBenefitInfo) { + val month = userOrderInfo.estimatedVisitDate.month.value + val day = userOrderInfo.estimatedVisitDate.dayOfMonth + println( + """ + |${month}월 ${day}일에 우테코 식당에서 받을 이벤트 혜택 미리 보기! + | + |<주문 메뉴> + |${userOrderInfo.userOrderMenu} + | + |<할인 전 총주문 금액> + |${userOrderInfo.totalPrice} + | + |<증정 메뉴> + |${benefitInfo.giveaway?.menuWithCount ?: "없음"} + | + |<혜택 내역> + |${benefitInfo.benefitList.joinToString("\n").ifEmpty { "없음" }} + | + |<총혜택 금액> + |${benefitInfo.totalBenefitAmount.toMinusString()} + | + |<할인 후 예상 결제 금액> + |${userOrderInfo.totalPrice - benefitInfo.totalDiscountAmount} + | + |<12월 이벤트 배지> + |${benefitInfo.eventBadge ?: "없음"} + """.trimMargin() + ) + } +}