-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[산타] 미니언 미션 제출합니다. #13
base: gusah009-main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package christmas.controller | ||
|
||
import christmas.service.ChristmasService | ||
import christmas.view.InputView | ||
import christmas.view.OutputView | ||
|
||
object ChristmasController { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. object에 대해 이리저리 서칭하면서 생각해봤습니다. 스프링-코틀린에서는 단순히 모듈성 기능을 하는, 즉 상태가 필요하지 않은 경우 object를 사용하여 싱글턴으로 관리할 수 있을 것 같아요. (미니언님이 만든 InputView, OutputView 처럼요!) 또한 말씀대로 object를 사용하면 인스턴스를 생성하는 코드를 따로 적지않아도 관리되는 측면에선 좋은 것 같으나, 생성 흐름을 제어하지 못한다는 점에서 단점이 있을 수 있을 것 같습니다. 따라서 인스턴스의 생명주기에 대한 흐름을 제어할 필요가 없다면 충분히 object를 사용해도 좋은 것 같다고 생각합니다! |
||
fun run( | ||
inputView: InputView, | ||
outputView: OutputView, | ||
christmasService: ChristmasService, | ||
) { | ||
outputView.printWelcomeMessage() | ||
val userOrderInfo = inputView.getUserOrderInfo() | ||
val benefitInfo = christmasService.getBenefitInfo(userOrderInfo) | ||
outputView.printUserEventBenefit(userOrderInfo, benefitInfo) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package christmas.event | ||
|
||
import christmas.menu.Menu | ||
import christmas.menu.MenuType | ||
import christmas.menu.MenuWithCount | ||
import christmas.model.Price | ||
import christmas.model.UserOrderInfo | ||
import org.assertj.core.util.VisibleForTesting | ||
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 | ||
|
||
@VisibleForTesting | ||
internal abstract fun isOwnSupported(userOrderInfo: UserOrderInfo): Boolean | ||
|
||
abstract fun getBenefit(userOrderInfo: UserOrderInfo): EventType | ||
|
||
protected abstract fun getPolicyName(): String | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. enum 클래스로 각 할인정책을 공통적으로 관리하는 방법이 있었네요! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()}" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
; | ||
|
||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 버디가 메뉴를 sealed 클래스로 구현했던 것 같습니다! 참고하시면 좋을 것 같습니다! |
||
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}개" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package christmas.model | ||
|
||
import christmas.validation.requireChristmas | ||
import java.text.DecimalFormat | ||
|
||
@JvmInline | ||
value class Price(val value: Int) { | ||
init { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 금액 도메인의 연산을 따로 만드셨군요! |
||
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package christmas.model | ||
|
||
import christmas.badge.Badge | ||
import christmas.event.EventType | ||
|
||
class UserBenefitInfo( | ||
val benefitList: List<EventType>, | ||
) { | ||
val giveaway: EventType.Giveaway? = benefitList | ||
.filterIsInstance<EventType.Giveaway>().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<EventType.Discount>() | ||
.sumOf { it.price.value } | ||
.toPrice() | ||
|
||
val eventBadge = Badge.of(totalBenefitAmount) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MenuWithCount>) { | ||
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") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<EventType>().apply { | ||
for (eventPolicy in eventPolicyList) { | ||
if (eventPolicy.isSupported(userOrderInfo)) { | ||
this.add(eventPolicy.getBenefit(userOrderInfo)) | ||
} | ||
} | ||
}.toList() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 고민하던 부분이었는데 이렇게 하면 init에서도 exception을 제어할 수 있네요🤨 |
||
if (!value) { | ||
throw ChristmasException(lazyMessage().toString()) | ||
} | ||
} | ||
|
||
class ChristmasException(message: String) : IllegalArgumentException() { | ||
override val message = ERROR_PREFIX + message | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
한글 객체 직관적이고 재미있네요 ㅎㅎ
저도 다음에 직관성이 필요하거나, 귀찮은(?) 요구사항이 있으면 한글로 만들어봐야겠습니다 ⚡️