Skip to content
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

[로또 2단계] 시아 미션 제출합니다. #135

Open
wants to merge 26 commits into
base: leeyerin0210
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
79bc0e4
refactor : Lotto의 가격 부분 삭제
Leeyerin0210 Feb 25, 2025
133dce9
refactor : 출력 로직 수정 , 로또 결과 산출 로직 수정
Leeyerin0210 Feb 25, 2025
02cb6d7
feat : 유저가 입력한 숫자의 로또를 수동으로 발행하는 기능 추가
Leeyerin0210 Feb 25, 2025
29794c2
fix : 수동과 자동 모든 로또가 출력되도록 변경
Leeyerin0210 Feb 25, 2025
d9ec1a0
refactor: 입력값 검증 방식 수정하여 반복 입력 가능하도록 변경
Leeyerin0210 Feb 25, 2025
66b2a02
refactor: 잘못된 입력값을 받았을 때 다시 입력 받도록 수정
Leeyerin0210 Feb 25, 2025
1907483
docs : 리드미 수정
Leeyerin0210 Feb 25, 2025
6a9e9d9
refactor : 뷰와 도메인이 너무 강하게 결합되지 않도록 수정
Leeyerin0210 Feb 25, 2025
4ed8d62
refactor : 로또 번호를 잘못 입력하면 바로 다시 입력 받도록 수정
Leeyerin0210 Feb 25, 2025
1db3c57
refactor : 출력 로직 일부 수정
Leeyerin0210 Feb 26, 2025
4485eac
refactor : sealed 클래스를 활용해서 오류 처리 일부 수정
Leeyerin0210 Feb 26, 2025
6dd8b3d
refactor : 오류 원인을 사용자가 파악할 수 있도록 출력 수정
Leeyerin0210 Feb 26, 2025
63bb963
feat : 로또 금액 단위의 금액을 입력하지 않으면 남는 돈을 출력하도록 추가
Leeyerin0210 Feb 26, 2025
e401cbd
fix : 테스트 함수 수정
Leeyerin0210 Feb 26, 2025
d07db8c
refactor: 기존 코드 구조를 폐기하고 새로운 설계를 시작
Leeyerin0210 Feb 27, 2025
422eaac
docs: 코드 작성 중 유의할 사항 기재
Leeyerin0210 Feb 27, 2025
0050e23
feat : 로또 넘버 객체 생성
Leeyerin0210 Feb 27, 2025
e7f5664
feat : 로또 객체 생성
Leeyerin0210 Feb 27, 2025
8eb1895
feat : amount 객체 생성
Leeyerin0210 Feb 28, 2025
8444009
feat : amount 객체 지불 함수 작성
Leeyerin0210 Feb 28, 2025
0fd886a
feat : rank 객채 및 winningLotto 객체 생성
Leeyerin0210 Feb 28, 2025
fece2a8
feat : 당첨금 계산 서비스 작성 및 파일 이동
Leeyerin0210 Mar 1, 2025
0220a90
feat : 로또를 자동으로 생성하는 로또 메이커 서비스 추가
Leeyerin0210 Mar 1, 2025
09eb61b
feat : 입출력 기능 추가
Leeyerin0210 Mar 2, 2025
19253ff
refactor : 생성 인터페이스 일관성 추가
Leeyerin0210 Mar 5, 2025
33d22ea
refactor : 비지니스 로직 분리 및 순회 횟수 감소
Leeyerin0210 Mar 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
## 기능 요구 사항
로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
로또 1장의 가격은 1000원이다.

## 유의할 사항

- 클래스 프로퍼티로 선언할때는 주의, 여러번 호출하거나 동시에 호출하면 꼬인다.
- 절차지향 x 객체끼리의 관계를 작성하자
- 객체를 어떻게 재활용할까? 혹은 의존성을 연결시킬까?
- private 함수를 테스트 하기 쉬운 방법 중 하나는 객체에게 역할을 넘기는 것
- 뷰와 도메인이 결합되지 않도록 주의
- 예외처리에 대해 너무 하루종일 생각하지 말자
- 코드의 일관성 유지
- 객체의 프로퍼티를 var로 해버리면 나중에 초기 유효성 검사를 무시할 가능성이 있다.
- 함수의 이름으로 어떤 기능을 하는지 유추할 수 있도록 하자
- 뭐든지 너무 매몰되지 말자
- 외부에서 유효성 검사를 호출해야만 할 수 있으면 의미가 없다.
## 기능 목록
### 로또 번호
- [x] 로또 번호는 1~45 사이이다.
Expand All @@ -11,11 +25,13 @@
- [x] 로또 번호는 중복되지 않는다.
- [x] 로또 번호는 오름차순으로 정렬된다.
### 구입금액 및 수량
- [x] 구입 금액은 천원 단위이다.
- [x] 구입 금액을 로또 가격으로 나누면 로또 수량이다.
- [x] 구입 금액은 최소 천원 이상이다.
- [x] 금액은 음수일 수 없다.
- [ ] 구매 후 남은 금액은 마지막에 출력한다.
### 로또 발행
- [x] 입력된 금액에 맞게 로또를 발행한다.
- [x] 입력된 횟수에 맞게 수동으로 로또를 입력받는다
- [x] 입력된 금액에 맞는 로또를 생성한다.
- [x] 남은 금액에 맞게 자동 로또를 발행한다.
### 당첨 통계
- [x] 당첨 로또 번호와 보너스 볼의 중복 여부를 확인한다.
- [x] 당첨 로또 번호와 로또 번호를 비교하여 순위를 리턴한다.
Expand All @@ -27,5 +43,3 @@
### 당첨금 계산
- [x] 당첨 총 금액을 모든 로또의 순위에 따라 계산한다
- [x] 입력된 금액과 비교하여 수익률을 도출한다


8 changes: 6 additions & 2 deletions src/main/kotlin/lotto/Application.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package lotto

import lotto.controller.LottoController
import lotto.view.InputView
import lotto.view.OutputView

fun main() {
val lottoController = LottoController()
lottoController.run()
val inputView = InputView()
val outputView = OutputView()
val controller = LottoController(inputView, outputView)
controller.run()
}
101 changes: 68 additions & 33 deletions src/main/kotlin/lotto/controller/LottoController.kt
Original file line number Diff line number Diff line change
@@ -1,53 +1,88 @@
package lotto.controller

import lotto.model.Amount
import lotto.model.Lotto
import lotto.model.LottoMachine
import lotto.model.LottoMatcher
import lotto.model.LottoNumber
import lotto.model.LottoNumbers
import lotto.model.PrizeCalculator
import lotto.domain.model.Amount
import lotto.domain.model.Lotto
import lotto.domain.model.LottoCreationResult
import lotto.domain.model.LottoNumber
import lotto.domain.model.Rank
import lotto.domain.model.WinningLotto
import lotto.domain.model.WinningLottoCreationResult
import lotto.domain.service.RankCalculator
import lotto.domain.service.WinningListMaker
import lotto.view.InputView
import lotto.view.Message
import lotto.view.OutputView

class LottoController(
private val inputView: InputView = InputView(),
private val outputView: OutputView = OutputView(),
private val inputView: InputView,
private val outputView: OutputView,
) {
private val amount = inputAmount()

fun run() {
val amount = getAmount()
val lottoMachine = LottoMachine(amount)
val publishedLotto = publishLotto(lottoMachine)
val count = getManualCount()
val manualLottoList = getLottoList(count)
val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)

outputView.printPurchaseResult(manualLottoList, autoLottoList)

val winningLotto = getWinningLotto()
val bonusNumber = getBonusNumber()
val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
showEarningRate(amount, lottoMatcher, publishedLotto)
val ranks = WinningListMaker(winningLotto).makeWinningList(manualLottoList + autoLottoList)
outputView.printResult(ranks, calculateEarningRate(ranks))
}

private fun getAmount(): Amount = Amount(inputView.getMoney())
private fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }

private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
outputView.printPublishedLotto(publishedLotto)
return publishedLotto
private fun getManualCount(): Int {
val manualCount = inputView.getManualCount()
if (amount.getCount(LOTTO_PRIZE) < manualCount) {
outputView.printErrorMessage(Message.errorCountExceeded())
return getManualCount()
}
return manualCount
}

private fun getWinningLotto(): Lotto {
val winningInput = inputView.getWinningLotto()
return Lotto(LottoNumbers(winningInput.map { number -> LottoNumber(number) }), Amount(LOTTO_PRIZE))
private fun getLottoList(count: Int): List<Lotto> {
inputView.messageManualLotto()
return List(count) { inputLotto() }
}

private fun getBonusNumber(): LottoNumber = LottoNumber(inputView.getBonusNumber())
private fun inputLotto(): Lotto {
val numbers = inputView.getManualLotto().mapNotNull { LottoNumber.valueOfOrNull(it) }
val result = Lotto.valueOf(numbers)
if (result is LottoCreationResult.Success) return result.lotto

outputView.printErrorMessage(Message.errorInvalidLotto())
return inputLotto()
}

private fun inputAmount(): Amount {
val amount = Amount.valueOfOrNull(inputView.getMoney())
if (amount != null) return amount

outputView.printErrorMessage(Message.errorInvalidAmount())
return inputAmount()
}

private fun getWinningLotto(): WinningLotto {
val numbers = inputView.getWinningLotto().mapNotNull { LottoNumber.valueOfOrNull(it) }
val bonusNumber = LottoNumber.valueOfOrNull(inputView.getBonusNumber())

if (bonusNumber == null) {
outputView.printErrorMessage(Message.errorInvalidBonusNumber())
return getWinningLotto()
}

val result = WinningLotto.valueOf(numbers, bonusNumber)
if (result is WinningLottoCreationResult.Success) return result.winningLotto

outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
return getWinningLotto()
}

private fun showEarningRate(
amount: Amount,
lottoMatcher: LottoMatcher,
publishedLotto: List<Lotto>,
) {
val result = lottoMatcher.matchLotto(publishedLotto)
val prizeCalculator = PrizeCalculator()
val earningRate = prizeCalculator.calculateEarningRate(amount.money, result)
outputView.printPrize(result, earningRate)
private fun calculateEarningRate(ranks: Map<Rank, Int>): Double {
val totalWinnings = RankCalculator().earningMoney(ranks)
return RankCalculator().calculateEarningRate(amount.money, totalWinnings)
}

companion object {
Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/lotto/domain/model/Amount.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lotto.domain.model

class Amount private constructor(
val money: Int,
) {
init {
require(money >= 0) { "[ERROR] 금액은 0 이상이어야 합니다." }
}

fun getCount(lottoPrize: Int): Int = money / lottoPrize

fun paymentOrNull(payMoney: Int): Amount? = runCatching { Amount(money - payMoney) }.getOrNull()

companion object {
fun valueOf(money: Int): Amount = Amount(money)

fun valueOfOrNull(money: Int): Amount? = runCatching { valueOf(money) }.getOrNull()
}
}
48 changes: 48 additions & 0 deletions src/main/kotlin/lotto/domain/model/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package lotto.domain.model

interface SortStrategy {
fun sort(numberList: List<LottoNumber>): List<LottoNumber>
}

class RandomSort : SortStrategy {
override fun sort(numberList: List<LottoNumber>): List<LottoNumber> = numberList.shuffled()
}

sealed class LottoCreationResult {
data class Success(
val lotto: Lotto,
) : LottoCreationResult()

sealed class Failure : LottoCreationResult() {
object NotSorted : Failure()

object InvalidCount : Failure()

object DuplicatedNumbers : Failure()
}
}

class Lotto private constructor(
val numberList: List<LottoNumber>,
) {
init {
require(numberList.sortedBy { it.value } == numberList) { "[ERROR] 로또 번호는 정렬된 상태여야 합니다." }
}

companion object {
const val LOTTO_NUMBER_QUANTITY = 6
private val LOTTO_NUMBERS: List<LottoNumber> = (1..45).map { LottoNumber.valueOf(it) }

fun valueOf(numberList: List<LottoNumber>): LottoCreationResult =
when {
numberList.size != LOTTO_NUMBER_QUANTITY -> LottoCreationResult.Failure.InvalidCount
numberList.distinctBy { it.value }.size != numberList.size -> LottoCreationResult.Failure.DuplicatedNumbers
else -> LottoCreationResult.Success(Lotto(numberList.sortedBy { it.value }))
}

fun valueOfOrNull(numberList: List<LottoNumber>): Lotto? = runCatching { Lotto(numberList.sortedBy { it.value }) }.getOrNull()

fun createRandom(sortStrategy: SortStrategy = RandomSort()): Lotto =
Lotto(sortStrategy.sort(LOTTO_NUMBERS).take(LOTTO_NUMBER_QUANTITY).sortedBy { it.value })
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/lotto/domain/model/LottoNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lotto.domain.model

data class LottoNumber(
val value: Int,
) {
init {
require(value in VALID_RANGE) { RANGE_ERROR }
}

companion object {
private val VALID_RANGE = 1..45
private const val RANGE_ERROR = "[ERROR] 범위 외의 값입니다."

fun valueOf(value: Int): LottoNumber = LottoNumber(value)

fun valueOfOrNull(value: Int): LottoNumber? = runCatching { valueOf(value) }.getOrNull()
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/lotto/domain/model/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lotto.domain.model

enum class Rank(
val countOfMatch: Int,
val winningMoney: Int,
val matchBonus: Boolean,
) {
FIRST(6, 2_000_000_000, false),
SECOND(5, 30_000_000, true),
THIRD(5, 1_500_000, false),
FOURTH(4, 50_000, false),
FIFTH(3, 5_000, false),
MISS(0, 0, false),
;

companion object {
fun valueOf(
countOfMatch: Int,
matchBonus: Boolean,
): Rank =
entries.firstOrNull {
(it.countOfMatch <= countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
} ?: MISS
}
}
56 changes: 56 additions & 0 deletions src/main/kotlin/lotto/domain/model/WinningLotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package lotto.domain.model

sealed class WinningLottoCreationResult {
data class Success(
val winningLotto: WinningLotto,
) : WinningLottoCreationResult()

sealed class Failure : WinningLottoCreationResult() {
object BonusNumberDuplicated : Failure()

object DuplicatedNumbers : Failure()

object NumberSizeError : Failure()
}
}

class WinningLotto private constructor(
private val lottoNumbers: List<LottoNumber>,
private val bonusNumber: LottoNumber,
) {
init {
require(lottoNumbers.size == WINNING_LOTTO_NUMBER_QUANTITY) { throw IllegalArgumentException("NumberSizeError") }
require(lottoNumbers.distinctBy { it.value }.size == lottoNumbers.size) { throw IllegalArgumentException("DuplicatedNumbers") }
require(!lottoNumbers.contains(bonusNumber)) { throw IllegalArgumentException("BonusNumberDuplicated") }
}

fun findRank(lotto: Lotto): Rank {
val countOfMatch = lottoNumbers.intersect(lotto.numberList).size
val bonusMatched = lotto.numberList.contains(bonusNumber)
return Rank.valueOf(countOfMatch, bonusMatched)
}

companion object {
const val WINNING_LOTTO_NUMBER_QUANTITY = 6

fun valueOf(
numbers: List<LottoNumber>,
bonusNumber: LottoNumber,
): WinningLottoCreationResult =
runCatching { WinningLotto(numbers, bonusNumber) }
.map { WinningLottoCreationResult.Success(it) }
.getOrElse { exception ->
when (exception.message) {
"NumberSizeError" -> WinningLottoCreationResult.Failure.NumberSizeError
"DuplicatedNumbers" -> WinningLottoCreationResult.Failure.DuplicatedNumbers
"BonusNumberDuplicated" -> WinningLottoCreationResult.Failure.BonusNumberDuplicated
else -> throw exception
}
}

fun valueOfOrNull(
numbers: List<LottoNumber>,
bonusNumber: LottoNumber,
): WinningLotto? = runCatching { WinningLotto(numbers, bonusNumber) }.getOrNull()
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/lotto/domain/service/RankCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lotto.domain.service

import lotto.domain.model.Rank

class RankCalculator {
fun earningMoney(winningList: Map<Rank, Int>): Int {
var money = 0
winningList.forEach {
money += it.key.winningMoney * it.value
}
return money
}

fun calculateEarningRate(
inputMoney: Int,
earningMoney: Int,
): Double {
require(inputMoney != 0) { "[ERROR] 입력 금액이 0입니다" }
return earningMoney.toDouble() / inputMoney.toDouble()
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/lotto/domain/service/WinningListMaker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lotto.domain.service

import lotto.domain.model.Lotto
import lotto.domain.model.Rank
import lotto.domain.model.WinningLotto

class WinningListMaker(
private val winningLotto: WinningLotto,
) {
fun makeWinningList(lottos: List<Lotto>): Map<Rank, Int> {
val ranks = lottos.map { winningLotto.findRank(it) }
val rankCounts =
Rank.entries
.associateWith { 0 }
.toMutableMap()
ranks.forEach {
rankCounts[it] = rankCounts.getOrDefault(it, 0) + 1
}
return rankCounts.filter { it.key != Rank.MISS }.toSortedMap(compareByDescending { it.ordinal })
}
}
Loading