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 27 commits into
base: leeyerin0210
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 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
00d4c70
refactor : 명확한 오류 원인 전달
Leeyerin0210 Mar 9, 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
12 changes: 12 additions & 0 deletions src/main/kotlin/lotto/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package lotto

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

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

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(
val inputView: InputView,
val outputView: OutputView,
) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feat : 입출력 기능 추가

이게 어떻게 봐야 입출력 기능추가인가요..?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

입출력 기능을 실행시키기 위한 컨트롤러도 포함되는 줄 알았습니다. 앞으로는 컨트롤러는 따로 커밋하도록 하겠습니다!

val amount = inputAmount()

fun run() {
val count = getManualCount()
val manualLottoList = getLottoList(count)
val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)

outputView.printPurchaseResult(manualLottoList, autoLottoList)

val winningLotto = getWinningLotto()
val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)

val sortedResults = sortResultsByOriginalRankOrder(ranks)
val totalWinnings = RankCalculator().earningMoney(ranks)
val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)

outputView.printResult(sortedResults, earningRate)
}

fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨트롤러의 함수 접근자를 public으로 열 필요가있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

19253ff
함수 접근자 확인하고 private으로 수정했습니다! 기본적으로 public으로 작성하는 안 좋은 습관이 있었던 것 같습니다.


fun getManualCount(): Int {
val manualCount = inputView.getManualCount()
return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
manualCount
} else {
outputView.printErrorMessage(Message.errorCountExceeded())
getManualCount()
}
}

fun getLottoList(count: Int): List<Lotto> {
inputView.messageManualLotto()
return List(count) { inputLotto() }
}

private fun inputLotto(): Lotto {
when (
val result =
Lotto.create(
inputView
.getManualLotto()
.mapNotNull { LottoNumber.createOrNull(it) }
.sortedBy { it.value },
)
) {
is LottoCreationResult.Success -> return result.lotto
is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커스텀 예외를 만들었지만 결국 동일한 출력이 발생하는 거라면 오버엔지니어링으로 보이네요!
이럴거면 굳이 타입을 만들 필요가 있었을까요?

runCatching { 
   val lotto = Lotto.valueOf(...)
}.getOrElse { ... }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

00d4c70

동일한 출력이 발생하지 않게, 출력 메세지를 세분화하였습니다.

}
return inputLotto()
}

private fun inputAmount(): Amount {
val amount = Amount.createOrNull(inputView.getMoney())
return amount ?: run {
outputView.printErrorMessage(Message.errorInvalidAmount())
inputAmount()
}
}

private fun getWinningLotto(): WinningLotto {
when (
val result =
WinningLotto.create(
inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 깊이가 너무 들어갔어요.
요구사항에 있듯이 코드 indent가 깊어지지 않도록 정리해보면 좋을 것 같아요

) {
is WinningLottoCreationResult.Success -> return result.winningLotto
is WinningLottoCreationResult.Failure.NumberSizeError -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
is WinningLottoCreationResult.Failure.BonusNumberDuplicated -> outputView.printErrorMessage(Message.errorInvalidBonusNumber())
is WinningLottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

또한 출력을 위한 메세지 오브젝트를 만들었는데, 이 오브젝트 내의 값을 컨트롤러가 직접 넣어서 뷰에게 보내는게 괜찮을까요?

왜 괜찮다고 생각하는지 궁금합니다.
Result 타입을 그대로 OutputView로 전달하면 안되는건가요?

Copy link
Author

@Leeyerin0210 Leeyerin0210 Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result 타입을 그대로 뷰에 전달하면 뷰가 너무 도메인에 대한 세부 정보를 알게 되는 것이 아닌가..해서 이렇게 작성했습니다!

}
return getWinningLotto()
}

private fun sortResultsByOriginalRankOrder(ranks: List<Rank>): List<Pair<Rank, Int>> =
Rank.entries
.filter { it != Rank.MISS }
.map { rank -> rank to ranks.count { it == rank } }
.reversed()
Copy link

@laco-dev laco-dev Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

당첨 로또가 몇개인지 계산하는 비즈니스 로직이 왜 컨트롤러에 있을까요?
알고리즘적으로 더 좋은 방법은 없을까요? 지금은 리스트를 몇번 순회하고 있는지 찾아보세요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

33d22ea
전의 코드에서는 Rank를 순회하면서 ranks 순회하면서 매번 맞는 값을 찾는 식이었는데, 새로 고친 코드에서는 ranks 한번만 순회해서 맞는 키값에 1을 더하는 식으로 바꾸었습니다!


companion object {
const val LOTTO_PRIZE = 1000
}
}
5 changes: 1 addition & 4 deletions src/main/kotlin/lotto/domain/model/Amount.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ class Amount private constructor(
}

companion object {
fun createOrNull(input: Int): Amount? {
if (input < 0) return null
return Amount(input)
}
fun createOrNull(input: Int): Amount? = if (input >= 0) Amount(input) else null
}
}
43 changes: 22 additions & 21 deletions src/main/kotlin/lotto/domain/model/Lotto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,39 @@ 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) { LOTTO_NOT_SORTED }
require(numberList.sortedBy { it.value } == numberList) { throw IllegalArgumentException() }
}

companion object {
const val LOTTO_NUMBER_QUANTITY = 6
const val LOTTO_NOT_SORTED = "[ERROR] 로또 번호가 정렬되지 않았습니다."
const val NUMBER_COUNT_ERROR = "[ERROR] 로또 번호 갯수가 ${LOTTO_NUMBER_QUANTITY}가 아닙니다"
const val NUMBER_DUPLICATE_ERROR = "[ERROR] 로또 번호가 중복됩니다."
private val LOTTO_NUMBERS: List<LottoNumber> = (1..45).map { it -> LottoNumber.valueOf(it) }

fun valueOf(numberList: List<LottoNumber>): Lotto {
require(numberList.size == LOTTO_NUMBER_QUANTITY) { NUMBER_COUNT_ERROR }
require(numberList.distinctBy { it.value }.size == numberList.size) { NUMBER_DUPLICATE_ERROR }
return Lotto(numberList)
}

fun createOrNull(numberList: List<LottoNumber>): Lotto? =
private val LOTTO_NUMBERS: List<LottoNumber> = (1..45).map { LottoNumber.valueOf(it) }

fun create(numberList: List<LottoNumber>): LottoCreationResult =
when {
numberList.size != LOTTO_NUMBER_QUANTITY -> null
numberList.distinctBy { it.value }.size != numberList.size -> null
else -> Lotto(numberList)
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 createRandom(sortStrategy: SortStrategy = RandomSort()): Lotto =
Lotto(
sortStrategy.sort(LOTTO_NUMBERS).slice(0..5).sortedBy {
it.value
},
)
Lotto(sortStrategy.sort(LOTTO_NUMBERS).take(LOTTO_NUMBER_QUANTITY).sortedBy { it.value })
}
}
6 changes: 2 additions & 4 deletions src/main/kotlin/lotto/domain/model/Rank.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ enum class Rank(
matchBonus: Boolean,
): Rank =
entries.firstOrNull {
(it.countOfMatch == countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
} ?: throw IllegalArgumentException(RANK_FIND_ERROR)

const val RANK_FIND_ERROR = "[ERROR] 순위를 찾을 수 없습니다"
(it.countOfMatch <= countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
} ?: MISS
}
}
45 changes: 25 additions & 20 deletions src/main/kotlin/lotto/domain/model/WinningLotto.kt
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
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,
) {
fun findRank(lotto: Lotto): Rank {
val countOfMatch =
lottoNumbers.intersect(lotto.numberList).size
val countOfMatch = lottoNumbers.intersect(lotto.numberList).size
val bonusMatched = lotto.numberList.contains(bonusNumber)
return Rank.valueOf(countOfMatch, bonusMatched)
}

companion object {
const val NUMBER_LIST_DUPLICATED = "[ERROR] 당첨 번호가 중복됩니다."
const val BONUS_NUMBER_DUPLICATED = "[ERROR] 보너스 번호가 중복됩니다."

fun valueOf(
inputNumbers: List<LottoNumber>,
bonusNumber: LottoNumber,
): WinningLotto {
require(!inputNumbers.contains(bonusNumber)) { BONUS_NUMBER_DUPLICATED }
require(inputNumbers.distinctBy { it.value } == inputNumbers) { NUMBER_LIST_DUPLICATED }
return WinningLotto(inputNumbers, bonusNumber)
}
const val WINNING_LOTTO_NUMBER_QUANTITY = 6

fun createOrNull(
inputNumbers: List<LottoNumber>,
fun create(
numbers: List<LottoNumber>,
bonusNumber: LottoNumber,
): WinningLotto? {
if (inputNumbers.contains(bonusNumber)) return null
if (inputNumbers.distinctBy { it.value } != inputNumbers) return null
return WinningLotto(inputNumbers, bonusNumber)
}
): WinningLottoCreationResult =
when {
numbers.size != WINNING_LOTTO_NUMBER_QUANTITY -> WinningLottoCreationResult.Failure.NumberSizeError
numbers.distinctBy { it.value }.size != numbers.size -> WinningLottoCreationResult.Failure.DuplicatedNumbers
numbers.contains(bonusNumber) -> WinningLottoCreationResult.Failure.BonusNumberDuplicated
else -> WinningLottoCreationResult.Success(WinningLotto(numbers, bonusNumber))
}
}
}
10 changes: 0 additions & 10 deletions src/main/kotlin/lotto/domain/service/LottoMaker.kt

This file was deleted.

6 changes: 1 addition & 5 deletions src/main/kotlin/lotto/domain/service/RankCalculator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ class RankCalculator {
inputMoney: Int,
earningMoney: Int,
): Double {
require(inputMoney != 0) { INPUT_MONEY_ZERO }
require(inputMoney != 0) { "[ERROR] 입력 금액이 0입니다" }
return earningMoney.toDouble() / inputMoney.toDouble()
}

companion object {
const val INPUT_MONEY_ZERO = "[Error] 입력 금액이 0입니다"
}
}
53 changes: 53 additions & 0 deletions src/main/kotlin/lotto/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package lotto.view

import lotto.view.InputMessage.ALERT_INPUT_LOTTO
import lotto.view.InputMessage.BONUS_NUMBER_INPUT_MESSAGE
import lotto.view.InputMessage.COUNT_INPUT_MESSAGE
import lotto.view.InputMessage.ERROR_MESSAGE
import lotto.view.InputMessage.MONEY_INPUT_MESSAGE
import lotto.view.InputMessage.WINNING_LOTTO_INPUT_MESSAGE

object InputMessage {
const val MONEY_INPUT_MESSAGE = "구입금액을 입력해 주세요."
const val WINNING_LOTTO_INPUT_MESSAGE = "\n지난 주 당첨 번호를 입력해 주세요."
const val BONUS_NUMBER_INPUT_MESSAGE = "보너스 볼을 입력해 주세요."
const val COUNT_INPUT_MESSAGE = "\n수동으로 구매할 로또 수를 입력해 주세요."
const val ALERT_INPUT_LOTTO = "수동으로 구매할 번호를 입력해 주세요."
const val ERROR_MESSAGE = "[ERROR] 입력이 올바르지 않습니다. 다시 입력해 주세요."
}

class InputView {
fun getMoney(): Int = getValidSingleNumber(MONEY_INPUT_MESSAGE)

fun getWinningLotto(): List<Int> = getValidMultipleNumbers(WINNING_LOTTO_INPUT_MESSAGE)

fun getBonusNumber(): Int = getValidSingleNumber(BONUS_NUMBER_INPUT_MESSAGE)

fun getManualCount(): Int = getValidSingleNumber(COUNT_INPUT_MESSAGE)

fun messageManualLotto() = println(ALERT_INPUT_LOTTO)

fun getManualLotto(): List<Int> = getValidMultipleNumbers(null)

private fun getValidSingleNumber(message: String): Int {
while (true) {
println(message)
val input = readln().trim()
val number = input.toIntOrNull()

if (number != null) return number
println(ERROR_MESSAGE)
}
}

private fun getValidMultipleNumbers(message: String?): List<Int> {
while (true) {
message?.let { println(it) }
val input = readln().trim()
val numbers = input.split(",").mapNotNull { it.trim().toIntOrNull() }

if (numbers.isNotEmpty() && numbers.size == input.split(",").size) return numbers
println(ERROR_MESSAGE)
}
}
}
Loading