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

[Step4] 4단계 - 로또(수동) PR 요청드립니다. #552

Open
wants to merge 21 commits into
base: dajeong
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9edc1be
[step4] 3단계 리뷰 반영 - 생성자 기본값
Dec 6, 2022
f528f8b
[step4] 3단계 리뷰 반영 - companion object 함수 변경
Dec 6, 2022
401a85d
[step4] 3단계 리뷰 반영 - 비효율적인 코드 수정
Dec 6, 2022
e093e7d
[step4] 3단계 리뷰 반영 - 불필요한 변수 삭제
Dec 6, 2022
f9d9dca
[step4] 4단계 기능목록 TODO 정리
Dec 8, 2022
d9d7cae
[step4] 지불 금액 클래스 생성
Dec 8, 2022
56aba68
[step4] 4단계 과제 내용 readme에 작성
Dec 12, 2022
77605aa
[step4] 로또 번호 클래스 생성, Int 타입을 포장할 Number 클래스 생성
Dec 12, 2022
89e133d
[step4] 입력 문자열 포장 클래스 생성
Dec 12, 2022
3291402
[step4] 패키지 위치 수정
Dec 12, 2022
6a205a9
[step4] 클래스명 변경 Number -> IntegerNumber
Dec 12, 2022
0db5155
[step4] 실수값 포장 클래스 DoubleNumber 생성
Dec 12, 2022
76bd302
[step4] 테스트 추가, 코드 정리
Dec 12, 2022
17716c8
[step4] 결과 출력 오류 수정, 테스트 보완
Dec 12, 2022
51f1799
[step4] Lotto와 LottoWinner 클래스를 합침, 불필요 코드 삭제, 예외 케이스 처리
Dec 12, 2022
0f56910
[step4] 수동 로또 개수, 번호 입력 기능
Dec 12, 2022
431d97a
[step4] 수동 로또 생성 기능
Dec 12, 2022
8a3e35b
[step4] 생성된 로또 출력 수정
Dec 12, 2022
533f5fb
[step4] 피드백 반영 - 모든 원시값 포장이 아닌 의미있는 값만 포장
Dec 27, 2022
c0e7831
[step4] 피드백 반영 - 당첨금을 위한 Money 클래스 생성
Dec 27, 2022
01d85e4
[step4] 피드백 반영 - 로또 구매 로직 변경
Dec 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
# kotlin-lotto

# 🚀 4단계 - 로또(수동)

## 기능 요구사항
- 현재 로또 생성기는 자동 생성 기능만 제공한다. 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다.
- 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다.

## 실행 결과
```
구입금액을 입력해 주세요.
14000

수동으로 구매할 로또 수를 입력해 주세요.
3

수동으로 구매할 번호를 입력해 주세요.
8, 21, 23, 41, 42, 43
3, 5, 11, 16, 32, 38
7, 11, 16, 35, 36, 44

수동으로 3장, 자동으로 11개를 구매했습니다.
[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]
[23, 25, 33, 36, 39, 41]
[1, 3, 5, 14, 22, 45]
[5, 9, 38, 41, 43, 44]
[2, 8, 9, 18, 19, 21]
[13, 14, 18, 21, 23, 35]
[17, 21, 29, 37, 42, 45]
[3, 8, 27, 30, 35, 44]

지난 주 당첨 번호를 입력해 주세요.
1, 2, 3, 4, 5, 6
보너스 볼을 입력해 주세요.
7

당첨 통계
---------
3개 일치 (5000원)- 1개
4개 일치 (50000원)- 0개
5개 일치 (1500000원)- 0개
5개 일치, 보너스 볼 일치(30000000원) - 0개
6개 일치 (2000000000원)- 0개
총 수익률은 0.35입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임)
```

## 프로그래밍 요구 사항
- 모든 원시값과 문자열을 포장한다.
- 예외 처리를 통해 에러가 발생하지 않도록 한다.
- 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
- Enum 클래스를 적용해 프로그래밍을 구현한다.
- 일급 컬렉션을 쓴다.
- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
- 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다.
- git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.

## 힌트
- 모든 원시값과 문자열을 포장한다.
- 로또 숫자 하나는 Int다. 이 숫자 하나를 추상화한 LottoNumber를 추가해 구현한다.
- 예외 처리를 통해 에러가 발생하지 않도록 한다.
- 참고로 코틀린에서는 아래와 같이 예외 처리를 한다. 장기적으로는 아래와 같이 예외 처리하는 걸 연습해 본다.
- 논리적인 오류일 때만 예외를 던진다.
- 논리적인 오류가 아니면 예외를 던지지 말고 null을 반환한다.
- 실패하는 경우가 복잡해서 null로 처리할 수 없으면 sealed class를 반환한다.
- 일반적인 코틀린 코드에서 try-catch를 사용하지 않는다.

---

# 🚀 3단계 - 로또(2등)

## 기능 요구사항
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/lotto/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# TODO

## 구현할 '로또(수동)' 기능 목록
- [x] 정수값 포장 클래스 생성
- [x] 실수값 포장 클래스 생성
- [x] 입력 문자열 포장 클래스 생성
- [x] 수동으로 구매할 로또 수 입력 기능
- [x] 수동으로 구매할 로또 번호 입력 기능
- [x] 수동 로또 생성 기능
- [x] 수동 로또, 자동 로또 출력 기능
- [x] 지불한 금액 클래스 생성
- [x] 로또 번호 클래스 생성
- [x] 입력 문자열 클래스 생성
- [x] 예외 케이스 검토하여 처리

## 구현할 '로또(2등)' 기능 목록
- [x] 보너스 볼 입력 기능
- [x] 당첨 통계에 2등 결과 추가
Expand Down
18 changes: 13 additions & 5 deletions src/main/kotlin/lotto/application/Application.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
package lotto.application

import lotto.domain.Lotto
import lotto.domain.LottoGenerator
import lotto.domain.LottoNumber
import lotto.domain.LottoResultService
import lotto.domain.LottoShop
import lotto.domain.LuckyNumbers
import lotto.util.RandomNumberGenerator
import lotto.view.InputView
import lotto.view.ResultView

class Application {
private val inputView = InputView()
private val resultView = ResultView()
private val lottoShop = LottoShop(LottoGenerator(RandomNumberGenerator()))
private val lottoGenerator = LottoGenerator(RandomNumberGenerator())

fun run() {

Choose a reason for hiding this comment

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

run함수가 비대해진거 같아요!
로또 구매, 로또확인, 결과출력 과 같이 함수로 분리해보면 어떨까요?

val inputPayment = inputView.inputPayment()
val lottoList = lottoShop.buyLotto(inputPayment)
resultView.printLotto(lottoList)
val manualLottoCount = inputView.inputManualLottoCount()
val manualNumberList = inputView.inputManualLottoNumbers(manualLottoCount)

val lottoShop = LottoShop(lottoGenerator, inputPayment)

val manualLottoList = lottoShop.buyManualLotto(manualNumberList)
val autoLottoList = lottoShop.buyAutoLotto()
resultView.printLottoList(manualLottoList, autoLottoList)
Comment on lines +24 to +26

Choose a reason for hiding this comment

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

개인적으로 수동 로또를 구매하고, 자동로또를 구매하는 로직은
구매에 대한 책임이 있는 LottoShop가 수행하여도 좋을거같아요!


val lottoList = manualLottoList + autoLottoList
val inputLuckyNumbers = inputView.inputLuckyNumbers()
val inputBonusNumber = inputView.inputBonusNumber()

val lottoResultService = LottoResultService(LuckyNumbers(inputLuckyNumbers, inputBonusNumber))
val lottoResultService = LottoResultService(Lotto(inputLuckyNumbers.integerNumberList.map { LottoNumber(it) }), LottoNumber(inputBonusNumber))

Choose a reason for hiding this comment

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

죄송합니다 🙏 이전 미션 코멘트에서
커뮤니케이션 미스가 있었던거 같아요 ㅠㅠ!

luckyNumbers는 lotto 클래스와 동일한 구조인거 같아요!
당첨번호의 luckyNumbers는 lotto 번호라고 보아도 되지않을가요?

LuckyNumbers 클래스가 아닌, LuckyNumbers객체의 List인 luckyNumbers 컬렉션을 의미하는 코멘트였습니다 ㅠㅠ

이전처럼 class LuckyNumbers( private val lotto: List<Lotto>, private val bonusNumber: Int)가 rank를 생성에 대한 책임은 이곳에 있어야하는게 맞는거같네요 ㅠ,ㅠ

Copy link
Author

Choose a reason for hiding this comment

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

아하 넵 ㅠㅠ 이해했습니다

val statistics = lottoResultService.inquireStatistics(inputPayment, lottoList)
resultView.printLottoStatistics(statistics)
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/lotto/common/IntegerNumberList.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package lotto.common

class IntegerNumberList(
val integerNumberList: List<Int>

Choose a reason for hiding this comment

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

수동 로또 구매를 위한 컬렉션이라면,
차라리 LottoTicket과 같은 네이밍은 어떨까요?

  • Ticket에도 숫자가 6개 인지도 필요하긴한거같아요

)
18 changes: 18 additions & 0 deletions src/main/kotlin/lotto/common/NumberString.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lotto.common

class NumberString(
private val string: String
) {
init {
require(string.isNotBlank()) { "값이 비어있습니다." }
require(isNumber()) { "숫자가 아닙니다. (입력값:$string)" }
}

private fun isNumber(): Boolean {
return string.toCharArray().all { it in '0'..'9' }
}

fun toIntegerNumber(): Int {
return string.toInt()
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/lotto/common/NumberStringList.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package lotto.common

class NumberStringList(
string: String
) {
val list = string.split(",").map { NumberString(it.trim()) }

fun toIntegerNumberList(): List<Int> {
return list.map { it.toIntegerNumber() }
}
}
14 changes: 12 additions & 2 deletions src/main/kotlin/lotto/domain/Lotto.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package lotto.domain

class Lotto(
val numbers: List<Int>
val lottoNumbers: List<LottoNumber>
) {
init {
require(numbers.size == LOTTO_NUMBERS_SIZE) { "로또 번호는 ${LOTTO_NUMBERS_SIZE}개가 필요합니다." }
require(lottoNumbers.size == LOTTO_NUMBERS_SIZE) { "로또 번호는 ${LOTTO_NUMBERS_SIZE}개가 필요합니다." }
require(lottoNumbers.toSet().size == LOTTO_NUMBERS_SIZE) { "번호에 중복이 있습니다." }
}

fun countHitNumbers(luckyLotto: Lotto): Int {
val count = lottoNumbers.count { luckyLotto.lottoNumbers.contains(it) }
return count
}

fun hasBonusNumber(bonusNumber: LottoNumber): Boolean {
return lottoNumbers.contains(bonusNumber)
}

companion object {
Expand Down
11 changes: 10 additions & 1 deletion src/main/kotlin/lotto/domain/LottoGenerator.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package lotto.domain

import lotto.common.IntegerNumberList
import lotto.util.NumberGenerator

class LottoGenerator(
Expand All @@ -9,7 +10,15 @@ class LottoGenerator(
return List(size) { Lotto(randomNumber()) }
}

private fun randomNumber(): List<Int> {
private fun randomNumber(): List<LottoNumber> {
return numberGenerator.generate(Lotto.LOTTO_START_NUMBER, Lotto.LOTTO_END_NUMBER, Lotto.LOTTO_NUMBERS_SIZE)
.map { LottoNumber(it) }
}

fun generate(numberList: List<IntegerNumberList>): List<Lotto> {
return numberList.map {
val lottoNumberList = it.integerNumberList.map { number -> LottoNumber(number) }
Lotto(lottoNumberList)
}
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/lotto/domain/LottoNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lotto.domain

data class LottoNumber(
val number: Int
) {
dajeong marked this conversation as resolved.
Show resolved Hide resolved

init {
require(number in LOTTO_MIN_NUMBER..LOTTO_MAX_NUMBER) { "로또 번호는 ${LOTTO_MIN_NUMBER}와 $LOTTO_MAX_NUMBER 사이 값 이여야 합니다." }
}

companion object {
const val LOTTO_MIN_NUMBER = 1
const val LOTTO_MAX_NUMBER = 45
}

override fun toString(): String {
return "$number"
}
}
34 changes: 17 additions & 17 deletions src/main/kotlin/lotto/domain/LottoRank.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,30 @@ package lotto.domain

enum class LottoRank(
val hitCount: Int,
val hasBonusNumber: Boolean,
val prizeMoney: Int
val prizeMoney: Money,
val hasBonusNumber: Boolean = false
) {
FIRST(6, false, 2000000000),
SECOND(5, true, 30000000),
THIRD(5, false, 1500000),
FOURTH(4, false, 50000),
FIFTH(3, false, 5000),
MISS(0, false, 0);
FIRST(hitCount = 6, prizeMoney = Money(2000000000)),
SECOND(hitCount = 5, prizeMoney = Money(30000000), hasBonusNumber = true),
THIRD(hitCount = 5, prizeMoney = Money(1500000)),
FOURTH(hitCount = 4, prizeMoney = Money(50000)),
FIFTH(hitCount = 3, prizeMoney = Money(5000)),
MISS(hitCount = 0, prizeMoney = Money(0));

companion object {
fun from(hitCount: Int, hasBonusNumber: Boolean): LottoRank {
return values().find { it.hitCount == hitCount && isHitBonusNumber(it, hasBonusNumber) } ?: return MISS
private fun isHitBonusNumber(it: LottoRank, hasBonusNumber: Boolean): Boolean {
if (it.hasBonusNumber) {
return hasBonusNumber
}
return true
}

private fun isHitBonusNumber(it: LottoRank, hasBonusNumber: Boolean): Boolean {
if (it.hasBonusNumber) {
return hasBonusNumber
}
return true
companion object {
fun from(hitCount: Int, hasBonusNumber: Boolean): LottoRank {
return values().find { it.hitCount == hitCount && it.isHitBonusNumber(it, hasBonusNumber) } ?: return MISS
}

fun winRanks(): List<LottoRank> {
return values().filter { it.prizeMoney > 0 }.reversed()
return values().filter { it.prizeMoney.amount > 0 }.reversed()
}
}
}
22 changes: 18 additions & 4 deletions src/main/kotlin/lotto/domain/LottoResultService.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
package lotto.domain

class LottoResultService(
private val luckyNumbers: LuckyNumbers,
private val luckyLotto: Lotto,
private val bonusNumber: LottoNumber

Choose a reason for hiding this comment

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

LottoResultService 라는 이름에 맞게
luckyLotto, luckyNumbers가 아닌
List<LottoRank>가 생성자에 있어야하지 않을까요?

) {
fun inquireStatistics(payment: Int, lottoList: List<Lotto>): LottoStatisticsTotal {
val lottoWinner = LottoWinner(luckyNumbers)
val winLottoList = lottoWinner.findWinLottoList(lottoList)
fun inquireStatistics(payment: Payment, lottoList: List<Lotto>): LottoStatisticsTotal {
val winLottoList = findWinLottoList(lottoList)
val lottoStatisticsService = LottoStatisticsService(payment, winLottoList)
return lottoStatisticsService.statistics()
}

private fun findWinLottoList(lottoList: List<Lotto>): List<LottoRank> {
return lottoList
.map { rank(it, bonusNumber) }
.filter { hasPrize(it) }
}

private fun rank(lotto: Lotto, bonusNumber: LottoNumber): LottoRank {
val hitCount = lotto.countHitNumbers(luckyLotto)
val hasBonusNumber = lotto.hasBonusNumber(bonusNumber)
return LottoRank.from(hitCount, hasBonusNumber)
}

private fun hasPrize(lottoRank: LottoRank) = lottoRank.prizeMoney.isPositive()
}
30 changes: 24 additions & 6 deletions src/main/kotlin/lotto/domain/LottoShop.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
package lotto.domain

import lotto.common.IntegerNumberList

class LottoShop(
private val lottoGenerator: LottoGenerator
private val lottoGenerator: LottoGenerator,
private var payment: Payment
) {
fun buyLotto(inputPayment: Int): List<Lotto> {
val lottoCount = calculateLottoCount(inputPayment)
return lottoGenerator.generate(lottoCount)
fun buyAutoLotto(): List<Lotto> {
val totalLottoCount = calculateLottoCount(payment)
return lottoGenerator.generate(totalLottoCount)
}

fun buyManualLotto(manualNumberList: List<IntegerNumberList>): List<Lotto> {
val totalLottoCount = calculateLottoCount(payment)
val manualLottoCount = manualNumberList.size

require(manualLottoCount <= totalLottoCount) { "수동 로또 개수가 지불 금액보다 많아 로또를 구입할 수 없습니다." }

val change = payment.charge(manualLottoCount * LOTTO_PRICE)
updatePayment(change)
return lottoGenerator.generate(manualNumberList)
}

private fun calculateLottoCount(payment: Payment): Int {
return payment.amount / LOTTO_PRICE
}

private fun calculateLottoCount(payment: Int): Int {
return payment / LOTTO_PRICE
private fun updatePayment(newPayment: Payment) {
this.payment = newPayment
}

companion object {
Expand Down
9 changes: 4 additions & 5 deletions src/main/kotlin/lotto/domain/LottoStatistics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import lotto.util.NumberUtil
class LottoStatistics(

Choose a reason for hiding this comment

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

LottoStatistics 클래스는 List의 일급컬렉션이라과 봐도될거같아요!
개인적으로 LottoRanks 같은 이름이 좀더 가독성에 좋을거같단 생각이 들어요!

private val winLottoList: List<LottoRank>
) {
private val prizeList: List<Int> = winLottoList.map { it.prizeMoney }
private val totalPrize: Int = prizeList.sum()
private val totalPrize: Int = winLottoList.sumOf { it.prizeMoney.amount }

fun earningRate(inputPayment: Int): Double {
val earningRate = totalPrize.toDouble() / inputPayment.toDouble()
fun earningRate(inputPayment: Payment): Double {
val earningRate = totalPrize.toDouble() / inputPayment.amount.toDouble()
return NumberUtil.floor(earningRate, EARNING_RATE_DECIMAL_PLACE)
}

Expand All @@ -25,6 +24,6 @@ class LottoStatistics(
}

companion object {
private const val EARNING_RATE_DECIMAL_PLACE = 2
private const val EARNING_RATE_DECIMAL_PLACE = 2.0
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/lotto/domain/LottoStatisticsService.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package lotto.domain

class LottoStatisticsService(
private val payment: Int,
private val payment: Payment,
private val winLottoList: List<LottoRank>
) {
fun statistics(): LottoStatisticsTotal {
Expand Down