-
Notifications
You must be signed in to change notification settings - Fork 107
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
[로또] 정남진 미션 제출합니다. #106
base: main
Are you sure you want to change the base?
[로또] 정남진 미션 제출합니다. #106
Changes from all commits
7057a26
63fb5f3
9f24f32
f2293a8
f3bc6ee
81bd6aa
2a7f6a8
dadc342
54d1611
b4ed4c7
19d2412
a1b9671
428fedc
cdfffd2
5f53524
a425938
4c840f2
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 +1,123 @@ | ||
# kotlin-lotto-precourse | ||
|
||
## 프로젝트 개요 | ||
|
||
이 프로젝트는 간단한 로또 발매기를 구현한 것입니다. 사용자가 입력한 금액에 따라 로또 번호를 발행하고, 당첨 번호를 입력받아 당첨 결과와 수익률을 계산합니다. | ||
|
||
--- | ||
|
||
## 기능 요구 사항 | ||
|
||
1. **금액 입력** | ||
- 금액을 정수형으로 입력하지 않으면 예외 발생 | ||
- 금액이 1000원으로 나누어떨어지지 않으면 예외 발생 | ||
- 금액이 0 이하일 경우 예외 발생 | ||
2. **금액에 따른 로또 번호 생성 및 출력** | ||
- 금액을 1000원으로 나눈만큼 랜덤으로 로또 번호를 생성 | ||
- 몇개의 로또를 구매했는지와 각각의 로또 번호를 오름차순으로 정렬하여 출력 | ||
- 하나의 로또 번호는 중복 없이 랜덤으로 6개 생성 | ||
- 유효한 로또 번호는 1부터 45 사이의 숫자 | ||
- 유효 범위 외의 숫자가 존재하거나 6개가 아닌 경우 예외 발생 | ||
3. **로또 당첨 번호 입력** | ||
- 로또 당첨 번호는 쉼표(,)로 구분된 6개의 숫자 | ||
- 위의 양식을 지키지 않는 경우 예외 발생 | ||
- 당첨 번호 중 1부터 45 사이의 숫자가 아닌 것이 있으면 예외 발생 | ||
4. **보너스 번호 입력** | ||
- 당첨 번호와 마찬가지로 1부터 45 사이의 숫자가 아닌 것이 있으면 예외 발생 | ||
- 보너스 번호와 당첨 번호가 중복이 될 경우 예외 발생 | ||
5. **결과 출력** | ||
- 생성된 로또와 당첨 번호로 당첨 결과 출력 | ||
6. **수익률 출력** | ||
- `당첨 금액 / 로또 구매 금액 * 100`으로 수익률을 계산하여 출력 | ||
- 수익률은 소수점 둘째 자리에서 반올림 | ||
7. **예외 처리** | ||
- 사용자가 잘못된 값을 입력시 알맞은 예외를 발생시키고 에러 메시지 출력 후 입력을 다시 받음 | ||
- 에러 메시지는 "[ERROR]"로 시작 | ||
|
||
--- | ||
|
||
## 입출력 요구 사항 | ||
|
||
### 입력 | ||
|
||
- **구입 금액** : 1,000원 단위로 입력, 1,000으로 나누어 떨어지지 않는 경우 예외 처리 | ||
```plaintext | ||
14000 | ||
``` | ||
|
||
- **당첨 번호** : 쉼표(,)로 구분된 6개의 숫자 입력 | ||
|
||
```plaintext | ||
1,2,3,4,5,6 | ||
``` | ||
|
||
- **보너스 번호** : 하나의 숫자 입력 | ||
|
||
```plaintext | ||
7 | ||
``` | ||
|
||
### 출력 | ||
|
||
- **발행 로또 번호** : 구매한 로또 수량과 각 번호를 오름차순으로 출력 | ||
|
||
```plaintext | ||
8개를 구매했습니다. | ||
[8, 21, 23, 41, 42, 43] | ||
[3, 5, 11, 16, 32, 38] | ||
[7, 11, 16, 35, 36, 44] | ||
[1, 8, 11, 31, 41, 42] | ||
[13, 14, 16, 38, 42, 45] | ||
[7, 11, 30, 40, 42, 43] | ||
[2, 13, 22, 32, 38, 45] | ||
[1, 3, 5, 14, 22, 45] | ||
``` | ||
|
||
- **당첨 내역 및 수익률**: 각 당첨 등수별 일치 개수와 수익률 출력 | ||
|
||
```plaintext | ||
3개 일치 (5,000원) - 1개 | ||
4개 일치 (50,000원) - 0개 | ||
5개 일치 (1,500,000원) - 0개 | ||
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 | ||
6개 일치 (2,000,000,000원) - 0개 | ||
총 수익률은 62.5%입니다. | ||
``` | ||
|
||
- 에러 메시지: 유효하지 않은 값 입력 시 에러 메시지 출력 | ||
|
||
```plaintext | ||
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다. | ||
``` | ||
|
||
### 실행 예시 | ||
|
||
```plaintext | ||
구입금액을 입력해 주세요. | ||
8000 | ||
|
||
8개를 구매했습니다. | ||
[8, 21, 23, 41, 42, 43] | ||
[3, 5, 11, 16, 32, 38] | ||
[7, 11, 16, 35, 36, 44] | ||
[1, 8, 11, 31, 41, 42] | ||
[13, 14, 16, 38, 42, 45] | ||
[7, 11, 30, 40, 42, 43] | ||
[2, 13, 22, 32, 38, 45] | ||
[1, 3, 5, 14, 22, 45] | ||
|
||
당첨 번호를 입력해 주세요. | ||
1,2,3,4,5,6 | ||
|
||
보너스 번호를 입력해 주세요. | ||
7 | ||
|
||
당첨 통계 | ||
--- | ||
3개 일치 (5,000원) - 1개 | ||
4개 일치 (50,000원) - 0개 | ||
5개 일치 (1,500,000원) - 0개 | ||
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 | ||
6개 일치 (2,000,000,000원) - 0개 | ||
총 수익률은 62.5%입니다. | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,13 @@ | ||
package lotto | ||
|
||
import lotto.data.random.pickLottoNumbers | ||
import lotto.domain.model.factory.LottoFactory | ||
import lotto.ui.Ui | ||
|
||
fun main() { | ||
// TODO: 프로그램 구현 | ||
val ui = Ui() | ||
val lottoFactory = LottoFactory(lottoNumberGenerator = ::pickLottoNumbers) | ||
val app = LottoApp(inputView = ui, outputView = ui, lottoFactory = lottoFactory) | ||
|
||
app.run() | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package lotto | ||
|
||
import lotto.domain.model.Lotto | ||
import lotto.domain.model.LottoWinPlace | ||
import lotto.domain.model.factory.LottoFactory | ||
import lotto.ui.InputView | ||
import lotto.ui.OutputView | ||
import lotto.util.keepCallingForSuccessResult | ||
|
||
class LottoApp( | ||
private val outputView: OutputView, | ||
private val inputView: InputView, | ||
private val lottoFactory: LottoFactory | ||
) { | ||
fun run() { | ||
val budget = keepCallingWithDefaultOnFailure(inputView::requestBudget) | ||
|
||
val amount = budget / Lotto.LOTTO_PRICE | ||
outputView.displayAmount(amount) | ||
|
||
val lottoes = lottoFactory.createLottoes(amount) | ||
outputView.displayLottoes(lottoes) | ||
|
||
val winningNumbers = keepCallingWithDefaultOnFailure(inputView::requestWinningNumbers) | ||
val bonusWinningNumber = keepCallingWithDefaultOnFailure { inputView.requestBonusWinningNumber(winningNumbers) } | ||
|
||
val lottoWinPlaces = lottoes.calculateAllLottoWinPlaces(winningNumbers, bonusWinningNumber) | ||
outputView.displayLottoResults(lottoWinPlaces, budget) | ||
} | ||
|
||
private fun List<Lotto>.calculateAllLottoWinPlaces( | ||
winningNumbers: List<Int>, | ||
bonusWinningNumber: Int | ||
): Map<LottoWinPlace, Int> = | ||
mapNotNull { it.calculateWinPlace(winningNumbers, bonusWinningNumber) }.groupingBy { it }.eachCount() | ||
|
||
private fun <R> keepCallingWithDefaultOnFailure(actionToCall: () -> Result<R>): R = keepCallingForSuccessResult( | ||
onFailure = outputView::displayExceptionMessage, | ||
actionToCall = actionToCall, | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package lotto.data.random | ||
|
||
import camp.nextstep.edu.missionutils.Randoms | ||
import lotto.domain.model.Lotto | ||
|
||
fun pickLottoNumbers(): List<Int> = Randoms.pickUniqueNumbersInRange( | ||
Lotto.VALID_LOTTO_NUMBER_RANGE.first, | ||
Lotto.VALID_LOTTO_NUMBER_RANGE.last, | ||
Lotto.VALID_LOTTO_NUMBER_LENGTH | ||
) | ||
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. data 레이어는 원격 및 로컬에 접근, 관리,처리등을 하는 레이어로 알고 있는데, 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.
데이터 레이어의 경우 원격 및 로컬에서 데이터를 가져오는 것 뿐만이 아니라 애플리케이션에서 사용될 데이터에 관해서 담당하는 레이어입니다. 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. 아 그렇군요 애플리케이션에서 담당할 데이터도 포함되는군요! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package lotto.domain | ||
|
||
import lotto.domain.model.LottoWinPlace | ||
|
||
private const val PERCENT = 100 | ||
|
||
fun calculateProfitRate(budget: Int, lottoWinPlaces: Map<LottoWinPlace, Int>): Float { | ||
val totalPrize = lottoWinPlaces.map { (lottoWinPlace, count) -> lottoWinPlace.prize * count }.sum().toFloat() | ||
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. sum을 사용한 시점에서 큰 값이 될 것 같습니다! 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. 네 소수 계산 때문에 사용했습니다. 그리고 말씀하신 것 처럼 큰 값이 되어 overflow가 발생할 여지에 대해서는 제가 마지막 날에 급하게 구현하느라 놓친점 같네요. |
||
return (totalPrize / budget) * PERCENT | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package lotto.domain.exception | ||
|
||
import lotto.domain.model.Lotto | ||
import lotto.domain.model.Lotto.Companion.VALID_LOTTO_NUMBER_LENGTH | ||
import lotto.domain.model.Lotto.Companion.VALID_LOTTO_NUMBER_RANGE | ||
|
||
object ExceptionMessages { | ||
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를 domain 패키지에 넣어둔 이유가 궁금하네요! 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. 네 말씀하신 것 처럼 common의 성격이 더 강한 것 같습니다. |
||
const val DEFAULT_EXCEPTION_MESSAGE = "오류가 발생 했습니다. 다시 입력해 주세요." | ||
const val BUDGET_NOT_DIVISIBLE_BY_LOTTO_PRICE = | ||
"금액이 로또 금액(${Lotto.LOTTO_PRICE})으로 나누어 떨어지도록 입력해주세요." | ||
const val BUDGET_NEEDS_TO_BE_BIGGER_THAN_ZERO = "금액은 0보다 커야합니다." | ||
const val INVALID_NUMBER_FORMAT = "올바른 숫자 형식을 입력해 주세요." | ||
const val INVALID_LOTTO_NUMBER_LENGTH = "로또 번호는 ${VALID_LOTTO_NUMBER_LENGTH}개여야 합니다." | ||
val NUMBER_OUT_OF_VALID_LOTTO_RANGE_EXISTS = | ||
"로또 번호는 ${VALID_LOTTO_NUMBER_RANGE.first}와 ${VALID_LOTTO_NUMBER_RANGE.last} 사이의 값이어야 합니다." | ||
const val DUPLICATE_NUMBER_EXISTS = "로또 번호에 중복이 없어야 합니다." | ||
const val DUPLICATE_BONUS_WINNING_NUMBER = "보너스 당첨 번호는 당첨 번호와 중복이 될 수 없습니다." | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package lotto.domain.model | ||
|
||
import lotto.domain.exception.ExceptionMessages | ||
|
||
class Lotto(private val numbers: List<Int>) { | ||
init { | ||
require(numbers.size == VALID_LOTTO_NUMBER_LENGTH) { ExceptionMessages.INVALID_LOTTO_NUMBER_LENGTH } | ||
require(numbers.distinct().size == numbers.size) { ExceptionMessages.DUPLICATE_NUMBER_EXISTS } | ||
require(numbers.all { number -> number in VALID_LOTTO_NUMBER_RANGE }) { | ||
ExceptionMessages.NUMBER_OUT_OF_VALID_LOTTO_RANGE_EXISTS | ||
} | ||
} | ||
|
||
fun calculateWinPlace(winningNumbers: List<Int>, bonusWinningNumber: Int): LottoWinPlace? { | ||
val matchedNumbers = getMatchedNumbers(winningNumbers) | ||
val isBonusWinningNumberMatched = bonusWinningNumber in numbers | ||
return LottoWinPlace.findLottoWinPlace(matchedNumbers.size, isBonusWinningNumberMatched) | ||
} | ||
|
||
private fun getMatchedNumbers(winningNumbers: List<Int>) = numbers.toSet().intersect(winningNumbers.toSet()) | ||
|
||
override fun toString(): String = numbers.sorted().toString() | ||
|
||
companion object { | ||
const val LOTTO_PRICE = 1_000 | ||
const val VALID_LOTTO_NUMBER_LENGTH = 6 | ||
val VALID_LOTTO_NUMBER_RANGE = 1..45 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package lotto.domain.model | ||
|
||
enum class LottoWinPlace( | ||
val prize: Int, | ||
val requiredMatchedNumberLength: Int, | ||
val shouldBonusWinningBeNumberMatched: Boolean, | ||
) { | ||
FIRST_PLACE( | ||
prize = 2_000_000_000, | ||
requiredMatchedNumberLength = 6, | ||
shouldBonusWinningBeNumberMatched = false | ||
), | ||
SECOND_PLACE( | ||
prize = 30_000_000, | ||
requiredMatchedNumberLength = 5, | ||
shouldBonusWinningBeNumberMatched = true | ||
), | ||
THIRD_PLACE( | ||
prize = 1_500_000, | ||
requiredMatchedNumberLength = 5, | ||
shouldBonusWinningBeNumberMatched = false | ||
), | ||
FOURTH_PLACE( | ||
prize = 50_000, | ||
requiredMatchedNumberLength = 4, | ||
shouldBonusWinningBeNumberMatched = false | ||
), | ||
FIFTH_PLACE( | ||
prize = 5_000, | ||
requiredMatchedNumberLength = 3, | ||
shouldBonusWinningBeNumberMatched = false | ||
); | ||
|
||
companion object { | ||
fun findLottoWinPlace( | ||
matchedNumberLength: Int, | ||
isBonusWinningNumberMatched: Boolean, | ||
): LottoWinPlace? { | ||
var candidates = entries.filter { it.requiredMatchedNumberLength == matchedNumberLength } | ||
|
||
if (candidates.hasMoreThanOneElement()) { | ||
candidates = candidates.filter { it.shouldBonusWinningBeNumberMatched == isBonusWinningNumberMatched } | ||
} | ||
|
||
return candidates.firstOrNull() | ||
} | ||
|
||
private fun List<LottoWinPlace>.hasMoreThanOneElement(): Boolean = size > 1 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package lotto.domain.model.factory | ||
|
||
import lotto.domain.model.Lotto | ||
import lotto.domain.random.LottoNumberGenerator | ||
|
||
class LottoFactory( | ||
private val lottoNumberGenerator: LottoNumberGenerator | ||
) { | ||
fun createLottoes(amount: Int): List<Lotto> = List(amount) { | ||
val lottoNumbers = lottoNumberGenerator.pickLottoNumbers() | ||
Lotto(lottoNumbers) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package lotto.domain.random | ||
|
||
fun interface LottoNumberGenerator { | ||
fun pickLottoNumbers(): List<Int> | ||
} | ||
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. impl이랑 다른 레이어에 있는 이유가 궁금합니다! 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. 랜덤값을 이용하여 로또를 생성하는 것은 로또앱의 비즈니스 로직으로 domain 레이어에 속하지만 랜덤값을 어떻게 가져오는지에 대해서 세부사항에 대해서는 감추어서 랜덤값 라이브러리 변경에 대해서 다른 코드에 영향을 미치지 않게 하기 위해서입니다. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html 위 글의 Crossing boundaries 부분을 읽어보시면 될 거 같습니다. 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. 아 이해 했습니다 생각보다 개념이 헷갈리는 부분이 많네요.. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package lotto.domain.validation | ||
|
||
import lotto.domain.exception.ExceptionMessages | ||
import lotto.domain.model.Lotto | ||
|
||
fun validateBonusWinningNumber(bonusWinningNumber: Int, winningNumbers: List<Int>) { | ||
require(bonusWinningNumber in Lotto.VALID_LOTTO_NUMBER_RANGE) { | ||
ExceptionMessages.NUMBER_OUT_OF_VALID_LOTTO_RANGE_EXISTS | ||
} | ||
require(bonusWinningNumber !in winningNumbers) { ExceptionMessages.DUPLICATE_BONUS_WINNING_NUMBER } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package lotto.domain.validation | ||
|
||
import lotto.domain.model.Lotto.Companion.LOTTO_PRICE | ||
import lotto.domain.exception.ExceptionMessages | ||
|
||
fun validateBudget(budget: Int) { | ||
require(budget > 0) { ExceptionMessages.BUDGET_NEEDS_TO_BE_BIGGER_THAN_ZERO } | ||
require(budget % LOTTO_PRICE == 0) { ExceptionMessages.BUDGET_NOT_DIVISIBLE_BY_LOTTO_PRICE } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package lotto.domain.validation | ||
|
||
import lotto.domain.model.Lotto | ||
|
||
fun validateWinningNumbers(winningNumbers: List<Int>) { | ||
// 로또 생성으로 당첨 번호 검증 | ||
Lotto(winningNumbers) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package lotto.ui | ||
|
||
interface InputView { | ||
fun requestBudget(): Result<Int> | ||
fun requestWinningNumbers(): Result<List<Int>> | ||
fun requestBonusWinningNumber(winningNumbers: List<Int>): Result<Int> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package lotto.ui | ||
|
||
import lotto.domain.model.Lotto | ||
import lotto.domain.model.LottoWinPlace | ||
|
||
interface OutputView { | ||
fun displayExceptionMessage(exception: Throwable) | ||
fun displayAmount(amount: Int) | ||
fun displayLottoes(lottoes: List<Lotto>) | ||
fun displayLottoResults(lottoWinPlaces: Map<LottoWinPlace, Int>, budget: Int) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package lotto.ui | ||
|
||
import lotto.ui.console.ConsoleInputView | ||
import lotto.ui.console.ConsoleOutputView | ||
|
||
class Ui : OutputView by ConsoleOutputView(), InputView by ConsoleInputView() | ||
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. 혹시 이 클래스의 역할은 무엇인가요? 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. 원래 해당 클래스에서 입출력을 모두 처리하다가 클래스가 커지는 것 같아서 InputView, OutputView로 나누었습니다. 기존의 main 함수 코드에 영향을 주지 않기 위해서 해당 클래스에 delegation을 이용해서 각각의 인터페이스를 구현하도록 했습니다. |
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.
이 부분에서 :: 로 참조를 사용하신거같은데 왜 참조로 사용하셨는지 알 수 있을까요?
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.
습관적으로 참조를 사용했네요.
Jetpack Compose에서 람다를 생성해서 넘겨주게 되면 recomposition이 필요하지 않은 상황에서도 발생하는 경우가 있어서 습관적으로 Compose에서 사용할 때 처럼 사용했습니다. trailing lambda를 이용하여 호출하여도 괜찮을 거 같습니다.