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

[로또] 곽상진 미션 제출합니다. #101

Open
wants to merge 59 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
cbe1c2a
docs: README.md 기능 목록 정리
DDangDin Oct 29, 2024
b374aa1
docs: README.md 입력 예외 처리 추가
DDangDin Oct 31, 2024
edbc8af
feat: 로또 구입 금액 입력하는 기능 추가
DDangDin Oct 31, 2024
ddbc8e3
docs: README.md 기능 진행 상황 업데이트
DDangDin Oct 31, 2024
e62260c
feat: 로또 구입 금액 예외 처리 추가
DDangDin Oct 31, 2024
5c46e12
docs: README.md 기능 진행상황 업데이트
DDangDin Oct 31, 2024
cecd603
feat: 로또 발매기에서 로또를 생성하는 기능 추가
DDangDin Oct 31, 2024
d0115aa
docs: README.md 기능 진행상황 업데이트
DDangDin Oct 31, 2024
849f228
feat: 로또 구입 금액에 맞게 로또 발행 기능 추가
DDangDin Oct 31, 2024
7b43241
docs: README.md 기능 진행상황 업데이트
DDangDin Oct 31, 2024
e1f4fc1
refactor: LottoMachine -> LottoGenerator 클래스명 수정
DDangDin Oct 31, 2024
df80913
feat: 발행한 로또 수량 및 오름차순 정렬된 번호 출력 추가
DDangDin Oct 31, 2024
5e8a5dd
docs: README.md 기능 진행상황 업데이트
DDangDin Oct 31, 2024
13f311c
fix: 로또 숫자 발행 시작 범위 수정
DDangDin Oct 31, 2024
d958ddb
feat: 당첨 번호, 보너스 번호를 입력 받는 기능 추가
DDangDin Oct 31, 2024
f432db7
docs: README.md 기능 진행상황 업데이트
DDangDin Oct 31, 2024
6021e91
docs: README.md 예외 처리 추가
DDangDin Oct 31, 2024
8a17141
docs: README.md 기능 분리
DDangDin Oct 31, 2024
d866127
feat: 로또 번호와 당첨 번호를 비교하여 당첨 카운트를 출력
DDangDin Oct 31, 2024
8e5834a
docs: README.md 기능 진행상황 업데이트
DDangDin Oct 31, 2024
838045a
test: Lotto 테스트 추가
DDangDin Oct 31, 2024
762f891
test: LottoGeneratorTest 테스트 코드 추가
DDangDin Oct 31, 2024
6f115ce
docs: README.md 기능명 수정
DDangDin Nov 1, 2024
d59fee2
feat: 로또 번호와 당첨 번호, 보너스 번호와 비교하는 기능 추가
DDangDin Nov 1, 2024
c83442d
refactor: LottoCounter 와 Lotto 클래스 리팩토링
DDangDin Nov 1, 2024
31afe32
docs: README.md 기능 진행상황 업데이트
DDangDin Nov 1, 2024
dd738f3
refactor: LottoCounter 클래스 명 수정 & 리팩토링
DDangDin Nov 1, 2024
aaf8b6d
test: LottoMatcherTest 테스트 코드 추가
DDangDin Nov 1, 2024
56aa11d
refactor: LottoMatch -> LottoNumberMatcher 클래스 명 수정
DDangDin Nov 1, 2024
32fb64f
feat: 로또 번호와 당첨 번호, 보너스 번호를 비교하여 순위를 매기는 기능 추가
DDangDin Nov 1, 2024
cfdbde9
docs: README.md 기능 진행상황 업데이트
DDangDin Nov 1, 2024
4d32272
feat: 수익률 계산 후 당첨 통계 내는 기능 구현
DDangDin Nov 1, 2024
6ce2cca
refactor: 수익률 계산하는 기능 수정
DDangDin Nov 2, 2024
f54b131
test: LottoResultTest 테스트 코드 추가
DDangDin Nov 2, 2024
6bcb6f9
fix: 로또 번호와 당첨 번호 비교하는 기능 수정
DDangDin Nov 2, 2024
6f09c26
test: LottoResultTest 테스트 코드 수정
DDangDin Nov 2, 2024
919cff1
test: LottoResultTest 테스트 코드 메서드 명 수정
DDangDin Nov 2, 2024
b0e9584
refactor: LottoResult 수익률 수정 수정 및 확장함수 추가
DDangDin Nov 2, 2024
92c3db4
docs: README.md 기능 추가
DDangDin Nov 4, 2024
5250349
docs: README.md 기능 수정 및 추가
DDangDin Nov 4, 2024
394143c
feat: 당첨 내역과 수익률을 출력하는 기능 구현
DDangDin Nov 4, 2024
fbef571
docs: README.md 기능 진행상황 업데이트
DDangDin Nov 4, 2024
6b2714e
refactor: 로또 최소, 최대 번호 상수 값을 LottoConstants에 작성
DDangDin Nov 4, 2024
ddddedb
feat: Lotto 클래스 예외 처리 추가
DDangDin Nov 4, 2024
a01a613
test: LottoTest 테스트 코드 추가
DDangDin Nov 4, 2024
5795d35
docs: README.md 기능 진행상황 업데이트 & 추가
DDangDin Nov 4, 2024
35c650c
refactor: MVC 패턴으로 리팩토링
DDangDin Nov 4, 2024
97becde
chore: MVC 패턴 적용으로 인한 테스트 코드 위치 수정
DDangDin Nov 4, 2024
c7180cb
refactor: MVC 패턴 적용 리팩토링
DDangDin Nov 4, 2024
ef19f99
refactor: MVC 패턴에서 각 계층 역할에 맞게 리팩토링
DDangDin Nov 4, 2024
7b60ae4
refactor: Validator 리팩토링
DDangDin Nov 4, 2024
ff9d841
docs: README.md 기능 진행상황 업데이트
DDangDin Nov 4, 2024
2d5f180
test: 역할 별 Validator 테스트 코드 추가
DDangDin Nov 4, 2024
276cb23
feat: 예외 발생 시 메시지 출력 후 다시 입력 받는 기능 구현
DDangDin Nov 4, 2024
5d386a2
docs: README.md 기능 진행상황 업데이트
DDangDin Nov 4, 2024
2aa9d0a
chore: 작은 수정
DDangDin Nov 4, 2024
2e0fcef
fix: 무한 루프 수정
DDangDin Nov 4, 2024
e226138
fix: 무한 루프 수정
DDangDin Nov 4, 2024
5ed3c3d
fix: 출력 텍스트 수정
DDangDin Nov 4, 2024
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
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,51 @@
# kotlin-lotto-precourse
# 우테코 프리코스 3주차 - 로또

**간단한 로또 발매기**를 구현

<br/>

## ⚙️ 기능 목록 정리

- **입출력 기능**
- **입력**
- [x] 로또 구입 금액을 1,000원 단위로 입력받는다
- [x] 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.
- [x] 보너스 번호를 입력 받는다.
- **입력 예외 처리**
- [x] 구입 금액이 1,000원으로 나누어 떨어지지 않는 경우
- [x] 구입 금액이 1,000원 보다 적은 금액일 경우
- [x] 당첨 번호가 쉼표로 구분되어 있지 않은 경우
- [x] 당첨 번호가 중복되는 경우
- [x] 보너스 번호가 당첨 번호와 중복되는 경우
- **입력 공통 예외 처리**
- [x] 숫자가 아닐 경우
- [x] 공백일 경우
- **출력**
- [x] 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다.
- [x] 당첨 내역을 출력한다.
- [x] **예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다.**
- [x] 수익률을 출력하고 로또 게임을 종료한다. 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)

<br/>

- **로또**
- [x] 로또 번호의 숫자 범위는 1~45까지이다.
- [x] 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- [x] 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- [x] 당첨은 1등부터 5등까지 있다.
- 당첨 기준과 금액
- 1등: 6개 번호 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원
- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
- [x] 로또 1장의 가격은 1,000원이다.
- [x] 사용자가 구매한 로또 번호와 당첨 번호(, 보너스 번호)를 비교한다.
- [x] 당첨 내역을 출력할 때 금액을 천 단위 당 쉼표로 구분한다.
- [x] 입력 부분에서 예외가 발생해도 예외 출력 후 계속해서 입력을 받는다.

- **로또 예외 처리**
- [x] 로또 번호는 최대 번호인 45를 초과하면 예외가 발생한다.
- [x] 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
- Exception이 아닌 IllegalArgumentException, IllegalStateException 등과 같은 명확한 유형을 처리한다.
10 changes: 9 additions & 1 deletion src/main/kotlin/lotto/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package lotto

import lotto.controller.LottoController
import lotto.view.LottoInputView
import lotto.view.LottoOutputView

fun main() {
// TODO: 프로그램 구현
val inputView = LottoInputView()
val outputView = LottoOutputView()
val lottoController = LottoController(inputView, outputView)

lottoController.start()
}
9 changes: 0 additions & 9 deletions src/main/kotlin/lotto/Lotto.kt

This file was deleted.

105 changes: 105 additions & 0 deletions src/main/kotlin/lotto/controller/LottoController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package lotto.controller

import lotto.model.Lotto
import lotto.model.LottoGenerator
import lotto.model.LottoRank
import lotto.model.LottoResult
import lotto.model.LottoResultDetail
import lotto.model.toViewData
import lotto.util.ErrorMessage
import lotto.util.validator.InputValidator
import lotto.util.validator.LottoValidator
import lotto.view.LottoInputView
import lotto.view.LottoOutputView

class LottoController(
private val inputView: LottoInputView,
private val outputView: LottoOutputView,
) {
private var purchasePrice = 0
private var lottoList = listOf<Lotto>()
private var winningNumbers = listOf<Int>()
private var bonusNumber = 0
private val printableRankList = mutableListOf<String>()
private val winningRankCountList = mutableListOf<Int>()

fun start() {
continueAfterException(outputView) { purchaseLotto() }
continueAfterException(outputView) { inputWinningNumbers() }
continueAfterException(outputView) { inputBonusNumber() }
continueAfterException(outputView) { showLottoResult() }
}

private fun purchaseLotto() {
outputView.outputPurchasePrice()
purchasePrice = inputView.getPurchasePrice()

val lottoGenerator = LottoGenerator(purchasePrice)
lottoList = lottoGenerator.generate()

val lottoListViewData = lottoList.toViewData()
outputView.newLine()
outputView.outputLottoList(lottoListViewData)
outputView.newLine()
}

private fun inputWinningNumbers() {
outputView.outputWinningNumber()
winningNumbers = inputView.getWinningNumbers()
require(LottoValidator.isNumbersLengthSix(winningNumbers)) {
ErrorMessage.LOTTO_NUMBERS_MUST_SIX_LETTERS.getMessage()
}
outputView.newLine()
}

private fun inputBonusNumber() {
outputView.outputBonusNumber()
bonusNumber = inputView.getBonusNumber()
require(InputValidator.isBonusNumberUnique(winningNumbers, bonusNumber)) {
ErrorMessage.LOTTO_NUMBERS_CONTAIN_BONUS_NUMBER.getMessage()
}
outputView.newLine()
}

private fun showLottoResult() {
val lottoResult = LottoResult(lottoList, winningNumbers, bonusNumber)
val lottoResultDetail = lottoResult.getResult()
val roundedRateOfReturn = lottoResultDetail.roundedRateOfReturnText

calculateRankList(lottoResultDetail)

outputView.outputLottoResult(
printableRankList = printableRankList,
winningRankCountList = winningRankCountList,
rateOfReturn = roundedRateOfReturn
)
outputView.newLine()
}

private fun calculateRankList(lottoResultDetail: LottoResultDetail) {
LottoRank.entries.forEach { rank ->
if (rank != LottoRank.NOTHING) {
val rankCount = lottoResultDetail.winningRankList.count { it == rank }
winningRankCountList.add(rankCount)
printableRankList.add(rank.print())
}
}
}
}

private fun continueAfterException(
outputView: LottoOutputView,
block: () -> Unit,
) {
var hasException = true
while (hasException) {
hasException = false
try {
block()
} catch (e: IllegalArgumentException) {
val errorMessage = e.localizedMessage
outputView.printErrorMessage(errorMessage)
hasException = true
}
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/lotto/model/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lotto.model

import lotto.util.ErrorMessage
import lotto.util.validator.LottoValidator

class Lotto(private val numbers: List<Int>) {
init {
require(LottoValidator.isNumbersLengthSix(numbers)) { ErrorMessage.LOTTO_NUMBERS_MUST_SIX_LETTERS.getMessage() }
require(LottoValidator.isNumbersUnique(numbers)) { ErrorMessage.LOTTO_NUMBERS_MUST_UNIQUE.getMessage() }
require(LottoValidator.isMaximumNumberExceeded(numbers)) { ErrorMessage.LOTTO_NUMBER_EXCEEDS_MAX.getMessage() }
}

fun getNumbers(): List<Int> = numbers

fun getSortedNumbers(): List<Int> = numbers.sorted()
}

fun List<Lotto>.toViewData(): List<List<Int>> = this.map { it.getNumbers() }
40 changes: 40 additions & 0 deletions src/main/kotlin/lotto/model/LottoGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package lotto.model

import camp.nextstep.edu.missionutils.Randoms.pickUniqueNumbersInRange
import lotto.util.ErrorMessage
import lotto.util.LottoConstants
import lotto.util.validator.LottoGeneratorValidator

class LottoGenerator(private val purchasePrice: Int) {
init {
require(LottoGeneratorValidator.isMoreThanOneThousand(purchasePrice)) {
ErrorMessage.PURCHASE_PRICE_MORE_THAN_THOUSAND.getMessage()
}
require(LottoGeneratorValidator.isThousandUnit(purchasePrice)) {
ErrorMessage.INVALID_PURCHASE_PRICE.getMessage()
}
}

fun generate(): List<Lotto> {
val lottoCount = getLottoCount()
val lottoList = List(lottoCount) {
generateOneLotto()
}
return lottoList
}

private fun getLottoCount(): Int = purchasePrice / LottoConstants.PRICE

private fun generateOneLotto(): Lotto {
val randomUniqueNumbers = pickUniqueNumbersInRange(
LottoConstants.MIN_NUMBER,
LottoConstants.MAX_NUMBER,
PICK_COUNT
)
return Lotto(randomUniqueNumbers)
}

companion object {
private const val PICK_COUNT = 6
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/lotto/model/LottoNumberMatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lotto.model

class LottoNumberMatcher {
// TODO: 두 개의 리스트가 모두 Set 이라면 intersect 메서드가 더 효율적일 것으로 예상
fun matchWithWinningNumbers(
lotto: Lotto,
winningNumbers: List<Int>,
): Int {
val lottoNumbers = lotto.getNumbers()
val matchCount = lottoNumbers.count { it in winningNumbers }
return matchCount
}

fun matchWithBonusNumber(
lotto: Lotto,
bonusNumber: Int,
): Boolean {
val lottoNumbers = lotto.getNumbers()
return lottoNumbers.contains(bonusNumber)
}
}
41 changes: 41 additions & 0 deletions src/main/kotlin/lotto/model/LottoRank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto.model

import lotto.view.LottoOutputText
import java.text.DecimalFormat

enum class LottoRank(val price: Int) {
NOTHING(0),
THREE_MATCHES(5000),
FOUR_MATCHES(50000),
FIVE_MATCHES(1500000),
FIVE_AND_BONUS_MATCHES(30000000),
SIX_MATCHES(2000000000);

fun print(): String = "${this.toDisplayName()} (${this.price.formatCurrency()}원)"

private fun toDisplayName(): String =
when (this) {
THREE_MATCHES -> LottoOutputText.THREE_MATCHES
FOUR_MATCHES -> LottoOutputText.FOUR_MATCHES
FIVE_MATCHES -> LottoOutputText.FIVE_MATCHES
FIVE_AND_BONUS_MATCHES -> LottoOutputText.FIVE_AND_BONUS_MATCHES
SIX_MATCHES -> LottoOutputText.SIX_MATCHES
NOTHING -> LottoOutputText.EMPTY
}

private fun Int.formatCurrency(): String {
val decimalFormat = DecimalFormat(THOUSAND_COMMA)
return decimalFormat.format(this)
}
}

fun Int.toLottoRank(): LottoRank =
when (this) {
3 -> LottoRank.THREE_MATCHES
4 -> LottoRank.FOUR_MATCHES
5 -> LottoRank.FIVE_MATCHES
6 -> LottoRank.SIX_MATCHES
else -> LottoRank.NOTHING
}

private const val THOUSAND_COMMA = "#,###"
66 changes: 66 additions & 0 deletions src/main/kotlin/lotto/model/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package lotto.model

import lotto.util.LottoConstants

class LottoResult(
private val lottoList: List<Lotto>,
private val winningNumbers: List<Int>,
private val bonusNumber: Int,
) {
private val lottoMatcher = LottoNumberMatcher()

fun getResult(): LottoResultDetail {
val lottoRankList = getLottoRankList()
val rateOfReturn = getRateOfReturn(lottoRankList)
val lottoResultDetail = LottoResultDetail(lottoRankList, rateOfReturn)
return lottoResultDetail
}

private fun getLottoRankList(): List<LottoRank> {
val lottoRankList = mutableListOf<LottoRank>()
lottoList.forEach { lotto ->
val winningCount = lottoMatcher.matchWithWinningNumbers(lotto, winningNumbers)
val hasBonusNumber = hasBonusNumber(winningCount, lotto)
val lottoLank = getLottoLank(winningCount, hasBonusNumber)
lottoRankList.add(lottoLank)
}
return lottoRankList
}

private fun getLottoLank(
winningCount: Int,
hasBonusNumber: Boolean,
): LottoRank {
if (hasBonusNumber) {
return LottoRank.FIVE_AND_BONUS_MATCHES
}
val winningLottoRank = winningCount.toLottoRank()
return winningLottoRank
}

private fun hasBonusNumber(
winningCount: Int,
lotto: Lotto,
): Boolean {
if (winningCount == FIVE_MATCHES) {
val hasBonusNumber = lottoMatcher.matchWithBonusNumber(lotto, bonusNumber)
return hasBonusNumber
}
return false
}

// 최종 수익률: (최종 수익 / 구입 금액) * 100
private fun getRateOfReturn(lottoRankList: List<LottoRank>): Double {
val purchasePrice = lottoList.size * LottoConstants.PRICE
val finalProfit = lottoRankList.sumOf { it.price }
val rateOfReturn =
((finalProfit.toDouble() / purchasePrice) * PERCENTAGE_MULTIPLIER).round(2)
return rateOfReturn
}

companion object {
private const val FIVE_MATCHES = 5
private const val PERCENTAGE_MULTIPLIER = 100
private fun Double.round(decimals: Int): Double = "%.${decimals}f".format(this).toDouble()
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/lotto/model/LottoResultDetail.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package lotto.model

data class LottoResultDetail(
val winningRankList: List<LottoRank>,
val rateOfReturn: Double
) {
val roundedRateOfReturnText: String
get() = String.format("%.1f", rateOfReturn)
}
Loading