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

[로또] 김채원 미션 제출합니다. #81

Open
wants to merge 26 commits into
base: main
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
cbe0933
docs: update README.md
Grove1212 Nov 1, 2024
d64cb36
docs: update README.md
Grove1212 Nov 3, 2024
49ab085
feat: add exception handling for lotto validation
Grove1212 Nov 3, 2024
e83e5aa
feat: add generate lotto
Grove1212 Nov 3, 2024
ca27f0f
feat: impliment lotto generation based on purchase amount
Grove1212 Nov 3, 2024
3c479cc
feat: add lottoResultChecker object and test
Grove1212 Nov 3, 2024
5609780
feat: add get purchase price and get lotto numbers
Grove1212 Nov 3, 2024
c6478b0
feat: add get bonusNumber function
Grove1212 Nov 3, 2024
ae452ee
feat: add LottoDraw class run method
Grove1212 Nov 3, 2024
4486516
feat: add OutputView class
Grove1212 Nov 3, 2024
08eaaea
fix(OutputView): 오탈자 수정
Grove1212 Nov 3, 2024
48bbe94
fix: 프로그래밍 요구 사항2 반영, mvc모델에 맞춰 파일 이동
Grove1212 Nov 3, 2024
3357876
refactor: 에러메시지 상수화
Grove1212 Nov 4, 2024
4d90189
feat: 당첨번호와 보너스 번호가 중복이면 에러처리
Grove1212 Nov 4, 2024
8e35461
feat: inputView에서 에러를 throw하는 구문을 클래스로 분리
Grove1212 Nov 4, 2024
0f31fd8
refactor: Lotto클래스 내부의 유효성 검사를 사용할 수 있도록 구현
Grove1212 Nov 4, 2024
b2c5ef3
refactor: Enum 클래스 적용
Grove1212 Nov 4, 2024
88c3407
refactor: 예외처리 추가
Grove1212 Nov 4, 2024
b400112
test: LottoResultChecker와 Lotto의 테스트코드 작성
Grove1212 Nov 4, 2024
ae6756f
test: StoreTest 수정
Grove1212 Nov 4, 2024
90f748f
test: InputValidateTest 추가
Grove1212 Nov 4, 2024
41d0d1e
style: 클래스들을 패키지에 정리
Grove1212 Nov 4, 2024
26ed47a
style: print값의 상수화
Grove1212 Nov 4, 2024
ed6fc51
chore: 피드백 주신 내용 검토
Grove1212 Nov 4, 2024
8cea67d
docs: 가독성있게 README.md 수정
Grove1212 Nov 4, 2024
1038ac8
docs: update README.md
Grove1212 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
155 changes: 155 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,156 @@
# kotlin-lotto-precourse

# 기능 구현 목록
### 1. 입력
- [x] 로또 구입 금액을 입력받는다.
- [x] [예외] 가격이 숫자가 아닌 경우.
- [x] [예외] 가격이 1000원 미만인 경우.

- [x] 당첨 번호를 입력받는다.
- [x] [예외] 문자인 경우.
- [x] [예외] 1~45사이의 숫자가 아닌 경우.
- [x] [예외] 숫자가 중복인 경우.
- [x] [예외] 6개의 숫자가 아닌 경우.

- [x] 보너스 번호를 입력받는다.
- [x] [예외] 가격이 숫자가 아닌 경우.
- [x] [예외] 1~45사이의 숫자가 아닌 경우.
- [x] [예외] 당첨번호와 중복될 경우

- [x] 사용자가 잘못된 값을 입력할 경우
- [x] `IllegalArgumentException`을 발생시킨다.
- [x] "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
- [x] `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다.

### 2. 로또 발행

- [x] 로또를 발행한다.
- [x] 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- [x] 오름차순으로 정렬한다.
- [x] 구입 금액에 해당하는 만큼 로또를 발행한다.
- [x] 로또 1장의 가격은 1,000원이다.

- [x] 로또가 알맞은 형식인지 검사한다.
- [x] [예외] 1~45사이의 숫자가 아닌 경우.
- [x] [예외] 숫자가 중복인 경우.
- [x] [예외] 6개의 숫자가 아닌 경우.
- [x] [예외] 오름차순이 아닌 경우.

### 3. 당첨 내역 저장

- [x] 당첨 여부를 비교한다.
- [x] 로또 리스트에서 로또 하나씩 사용자가 구매한 로또 번호와 당첨 번호를 비교한다.
- [x] 몇등을 했는지 당첨 내역을 구한다.

- [x] 수익률을 구한다.
- [x] 당첨 내역을 토대로 수익을 구한다.

### 4. 출력
- [x] n개일치가 몇 번 있는지 출력한다.
- [x] 수익률을 출력한다.

# 입출력 요구 사항

## 입력

- 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.

`14000`

- 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.

`1,2,3,4,5,6`

- 보너스 번호를 입력 받는다.

`7`

## 출력

- 발행한 로또 수량 및 번호를 출력한다.
- 로또 번호는 오름차순으로 정렬하여 보여준다.

```
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]
```

- 당첨 내역을 출력한다.

```
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
```

- 수익률을 출력한다.
- 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
- `총 수익률은 62.5%입니다.`

- 예외 상황 시 에러 문구를 출력해야 한다.
- 단, 에러 문구는 "[ERROR]"로 시작해야 한다.
`[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.`

# 프로그래밍 요구 사항 1

- Kotlin 1.9.24에서 실행 가능해야 한다.
- Java 코드가 아닌 Kotlin 코드로만 구현해야 한다.
- 프로그램 실행의 시작점은 Application의 main()이다.
- build.gradle.kts 파일은 변경할 수 없으며, 제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.
- 프로그램 종료 시 System.exit() 또는 exitProcess()를 호출하지 않는다.
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.
- 코틀린 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 Kotlin Style Guide를 원칙으로 한다.

## 프로그래밍 요구 사항 2

- [x] indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- [x] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.

- JUnit 5와 AssertJ를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다.
- 테스트 도구 사용법이 익숙하지 않다면 아래 문서를 참고하여 학습한 후 테스트를 구현한다.
```
JUnit 5 User Guide
AssertJ User Guide
AssertJ Exception Assertions
Guide to JUnit 5 Parameterized Tests
```

## 프로그래밍 요구 사항 3

- [x] 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
- [x] 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
- [x] else를 지양한다.
- 때로는 if/else, when문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.
- 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
- [x] Enum 클래스를 적용하여 프로그램을 구현한다.
- [x] 구현한 기능에 대한 단위 테스트를 작성한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
- 단위 테스트 작성이 익숙하지 않다면 LottoTest를 참고하여 학습한 후 테스트를 작성한다.

# 피드백

- [x] 기능 목록을 재검토한다.
- [x] 기능 목록을 작성할 때 클래스 설계와 구현, 메서드 설계와 구현 등은 포함하지 않는다.
- [x] 예외상황도 함께 정리한다.
- [x] 기능 목록을 업데이트한다. 기능을 구현하면서 문서를 지속적으로 업데이트한다.
- [X] **문자열이나 숫자 값을 하드 코딩하지 않는다. 상수를 정의하고 의미있는 이름을 부여하여 해당 값이 어떤 역할을 하는지 명확히 드러낸다.**
- [x] 구현 순서도 코딩 컨벤션이다.
- [x] 프로퍼티, init블록, 부 생성자, 메서드, 동반 객체 순으로 작성한다.
- [x] 변수 이름에 자료형은 사용하지 않는다. 변수 이름은 의미를 명확히 드러낼 수 있도록 한다.
- [x] 테스트를 작성하는 이유에 대해 본인의 경험을 토대로 정리해본다. (회고때 포함)
- [x] 테스트 작성 과정을 통해 구현한 기능의 문제를 빠르게 발견할 수 있을 뿐만 아니라, 코드의 구조와 의도를 명확히 이해하는 데도 도움을 받을 수 있다. 학습 도구로도 활용할 수 있는데, 수 많은
테스트의 장점 중 본인이 가장 공감하는 작성 이유를 작성해 본다.
- [x] 처음부터 큰 단위 테스트를 만들지 않는다. → 테스트 주도 개발을 하자
- [x] 작은 단위 테스트 : 무작위 값이 4 이상이면 자동차가 전진한다.
- [x] 큰 단위 테스트 : 자동차 경주 게임을 시작하여, 사용자가 이름과 진행횟수를 입력하고, 게임을진행한 후 결과를 확인한다.

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.

네 편하게 참고하세요 피드백 감사합니다!

8 changes: 8 additions & 0 deletions 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.LottoDraw
import lotto.view.InputView
import lotto.view.OutputView

fun main() {
// TODO: 프로그램 구현
val inputView = InputView()
val outputView = OutputView()
val lottoDraw = LottoDraw()
lottoDraw.run(inputView, outputView)
}
9 changes: 0 additions & 9 deletions src/main/kotlin/lotto/Lotto.kt

This file was deleted.

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

import lotto.model.Lotto
import lotto.model.Store
import lotto.util.ErrorMessages

Choose a reason for hiding this comment

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

Lotto랑 ErrorMessage를 사용하지 않는데 import가 남아있습니다!

Copy link
Author

Choose a reason for hiding this comment

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

앗 미처 삭제하지 못했네요 알려주셔서 감사해요!

import lotto.view.InputView
import lotto.view.OutputView

class LottoDraw {
fun run(inputView: InputView, outputView: OutputView) {
val price = inputView.getPrice()

val store = Store()
val lottos = store.buyLotto(price)

val myLotto = inputView.getMyLotto()
val myBonus = inputView.getBonusNumber(myLotto)

outputView.purchasedMessage(store.numberOfLottoPurchased)
outputView.purchasedLotto(lottos)

val ranking = LottoResultChecker.checkWinningStatus(lottos, myLotto, myBonus)
val profitRatio = LottoResultChecker.calculateProfitRate(price, ranking)
outputView.resultStatistics(ranking, profitRatio)
}
}
Comment on lines +9 to +26

Choose a reason for hiding this comment

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

컨트롤러임을 명확히 표현하기 위해 class 이름에 Controller를 붙히는것도 좋아 보이네요 !

요 함수가 전체적인 비즈니스 흐름을 컨트롤 한다고 예상되는데
각 플로우마다 별도의 함수로 묶어서 이 함수에서 모두 실행시키는 방법은 어떨까요 ?

문제 요구사항에 함수의 라인이 15라인 이내로 제한하는데 14라인이면 살짝 위태 위태 한 것 같아요 ㅎㅎ
기능별로 별도의 함수로 만드는게 좋을 것 같아요 !

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

import lotto.model.Lotto

object LottoResultChecker {

fun checkWinningStatus(lottos: List<Lotto>, myLotto: Lotto, myBonus: Int): List<Int> {
val ranking = MutableList(6) { 0 }

for (lotto in lottos) {
val (duplicateCount, isBonus) = compareLotto(lotto, myLotto, myBonus)
when (duplicateCount) {
3 -> ranking[5]++
4 -> ranking[4]++
5 -> if (isBonus) ranking[2]++ else ranking[3]++
6 -> ranking[1]++
}
}
return ranking
Comment on lines +8 to +19

Choose a reason for hiding this comment

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

여기 있는 넘버들이 각각의 어떤 의미인지 상수나 enum class로 표현하면 좋을 것 같아요 !

}

fun compareLotto(purchasedLotto: Lotto, myLotto: Lotto, myBonus: Int): Pair<Int, Boolean> {
val sameValueOfLotto = purchasedLotto.getNumbers().intersect(myLotto.getNumbers().toSet())
val duplicateCount = sameValueOfLotto.size
val isBonus = myBonus in purchasedLotto.getNumbers()

return Pair(duplicateCount, isBonus)
}

fun calculateProfitRate(money: Int, ranking: List<Int>): Double {
val profit = RankPerPrice.entries.toTypedArray().sumOf { rank ->
(ranking.getOrNull(rank.ranked) ?: 0) * rank.price
}
val profitRate = profit.toDouble() / money
return profitRate * 100
}

enum class RankPerPrice(val ranked: Int, val price: Int) {
FIRST(1, 2000000000),
SECOND(2, 30000000),
THIRD(3, 1500000),
FORTH(4, 50000),
FIFTH(5, 5000);
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/lotto/model/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package lotto.model

import lotto.util.ErrorMessages

class Lotto(private val numbers: List<Int>) {
init {
require(numbers.size == 6) { ErrorMessages.INVALID_NUMBER_SIZE }
require(numbers.distinct().size == numbers.size) { ErrorMessages.DUPLICATE_NUMBERS }
require(numbers.all { it in 1..45 }) { ErrorMessages.OUT_OF_RANGE }
require(numbers == numbers.sorted()) { ErrorMessages.NOT_SORTED }
Comment on lines +7 to +10
Copy link

@chanho0908 chanho0908 Nov 6, 2024

Choose a reason for hiding this comment

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

요기두 각각의 숫자가 의미하는바를 명확히 표현해 주시면 좋을 것 같습니다 !

}

fun getNumbers(): List<Int> {
return numbers
}

Choose a reason for hiding this comment

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

numbers가 private로 되어있고 함수로 반환하는 이유는 List와 같은 객체는 외부에서 변경 할 수 있는 가능성이 있어서 라고 생각해서 numbers를 다른 방식으로 반환 시키는 형태가 좋을것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

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

이번에 피드백 보니까 확실히 그런 것 같더라구요 객체는 객체답게 하는 것을 명심해야겠어요!


override fun toString(): String {
return numbers.toString()
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/lotto/model/Store.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lotto.model

import camp.nextstep.edu.missionutils.Randoms

class Store {
var numberOfLottoPurchased: Int = 0
val purchasedLottos = mutableListOf<Lotto>()
Grove1212 marked this conversation as resolved.
Show resolved Hide resolved

fun buyLotto(money: Int): List<Lotto> {
numberOfLottoPurchased = money / 1000
repeat(numberOfLottoPurchased) {
purchasedLottos.add(generateLotto())
}
return purchasedLottos
}

fun generateLotto(): Lotto {
var numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6)
while (numbers.distinct().size != numbers.size) {
numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6)
}

Choose a reason for hiding this comment

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

Randoms.pickUniqueNumbersInRange(1, 45, 6) 자체가 1~45에 중복되지 않는 6개의 숫자를 뽑는 함수여서 while에서 중복체크를 하지않아도 될 것 같습니다!

val lotto = Lotto(numbers.sorted())
return lotto
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/lotto/util/ErrorMessages.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package lotto.util

object ErrorMessages {
const val INVALID_NUMBER_SIZE = "[ERROR] 로또 번호는 6개여야 합니다."
const val DUPLICATE_NUMBERS = "[ERROR] 로또 번호는 중복되지 않아야 합니다."
const val OUT_OF_RANGE = "[ERROR] 모든 로또 번호는 1부터 45 사이여야 합니다."
const val NOT_SORTED = "[ERROR] 로또 번호는 오름차순으로 정렬되어 있어야 합니다."
const val NULL_PRICE = "[ERROR] 구입금액을 입력해 주세요."
const val NOT_INT = "[ERROR] 입력이 숫자가 아닙니다."
const val MINIMUM_PRICE = "[ERROR] 로또 한 개의 가격은 1000원 이상이어야 합니다"
const val NULL_LOTTO_NUMBER = "[ERROR] 당첨 번호를 입력해 주세요."
const val NULL_BONUS_NUMBER = "[ERROR] 보너스 번호를 입력해 주세요."
const val DUPLICATED_BONUS = "[ERROR] 보너스 번호는 로또 번호와 중복될 수 없습니다."
}
18 changes: 18 additions & 0 deletions src/main/kotlin/lotto/util/InputValidate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lotto.util

import lotto.model.Lotto

object InputValidate {
fun checkPrice(input: String, price: Int?) {

Choose a reason for hiding this comment

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

해당 오브젝트가 input에 관련한 예외처리라서 input만 받아서 해당 함수에서 toIntOrNull()을 사용해도 될것 같은데 input과 price를 둘다 매개변수로 받는 이유가 궁금합니다!
validate인데 parser의 역할을 한다고 생각해서 분리하신거면 parser를 따로 만드는것도 좋을것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

validate인데 여기서 분리하는것도 굳이 라는 생각이 들어서 그냥 매개변수로 둘 다 들고왔는데 그것도 좋은 생각인 것 같아요!

require(input != "") { ErrorMessages.NULL_PRICE }
require(price != null) { ErrorMessages.NOT_INT }
require(price >= 1000) { ErrorMessages.MINIMUM_PRICE }
}
Comment on lines +6 to +10
Copy link

@chanho0908 chanho0908 Nov 6, 2024

Choose a reason for hiding this comment

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

문제 요구사항에 천원 단위로 입력받아야 했던 것으로 기억하는데
천원 단위가 아닐 때에 대한 에러 처리가 되었을까요 ? 요기만 봐서는 구현이 안된 것 같다고 생각이 들어서요 !

정수형을 입력 받을 때 Int로 표현 가능한 범위가 초과되면 NumberFormatException이 발생합니다
요것두 체크해보시면 좋을 것 같습니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

넵! 값의 범위는 생각지 못했네요 알려주셔서 감사합니다 ㅎㅎ
제가 생각했을 땐 어차피 1200원을 내도 한개밖에 못사는데 유하게 처리하자! 라고 생각해서 이렇게 했는데 다음부터는 기능 요구사항을 꼼꼼히 읽어봐야겠네요!


fun checkMyBonus(input: String, bonusNumber: Int?, myLotto: Lotto) {
require(input != "") { ErrorMessages.NULL_BONUS_NUMBER }
require(bonusNumber != null) { ErrorMessages.NOT_INT }
require(bonusNumber in 1..45) { ErrorMessages.OUT_OF_RANGE }
require(bonusNumber !in myLotto.getNumbers()) { ErrorMessages.DUPLICATED_BONUS }
}
}
Loading