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단계 - 로또(자동) 리뷰 요청드립니다. #1080

Open
wants to merge 21 commits into
base: bird798
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
e3ede51
refactor : 1단계 피드백 반영
chan798 Nov 24, 2024
3bfd862
refactor : 1단계 피드백 반영
chan798 Nov 24, 2024
5da5058
refactor : 1단계 피드백 반영
chan798 Nov 24, 2024
296d96f
docs(README): 기능 요구사항 및 기능 목록 작성
chan798 Nov 26, 2024
5340882
test : TestCase 추가
chan798 Nov 26, 2024
0449a0f
feat : InputView, ResultView 기능 추가
chan798 Nov 26, 2024
db4af4a
feat : 공통 상수값 추가
chan798 Nov 26, 2024
3009836
feat : 로또 구매 기능 추가
chan798 Nov 26, 2024
4d46a4f
feat : 로또 당첨 확인 및 수익률 분석 기능 추가
chan798 Nov 26, 2024
098e3da
feat : 어플리케이션 계충 추가.
chan798 Nov 26, 2024
bce244b
docs(README) : 기능 구현목록표 수정.
chan798 Nov 26, 2024
329cd84
refactor(InputView) : InputView의 리턴값을 Non-Nullable로 수정
chan798 Nov 26, 2024
36f29f6
refactor(InputView) 당첨번호의 유효성을 체크하는 기능을 WinningNumbers에서 이동
chan798 Nov 27, 2024
8e683c1
refactor: 로또 당첨 정보를 Lotto Class에서 변경.
chan798 Nov 27, 2024
33abe6d
refactor: 불필요한 LottoMarketService.kt 제거
chan798 Nov 27, 2024
233cd73
refactor: WinningChecker.kt 기능을 Lottos로 이동
chan798 Nov 27, 2024
b70f863
refactor: Lotto의 난수 발생기능 이관
chan798 Nov 27, 2024
47ba72e
refactor: Lotto의 당첨정보 속성을 제거하고, Map으로 변경
chan798 Nov 27, 2024
bdc9d43
refactor: WinningStatistics Class의 변경 사항 적용.
chan798 Nov 27, 2024
b2b21c8
refactor: 숫자 길이의 유효성을 생성자에서 체크하도록 수정.
chan798 Nov 27, 2024
5c1d433
test: TestCase 수정.
chan798 Nov 27, 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
7 changes: 7 additions & 0 deletions src/main/kotlin/lotto/LottoApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package lotto

import lotto.application.LottoController

fun main() {
LottoController.start()
}
118 changes: 118 additions & 0 deletions src/main/kotlin/lotto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## 문자열 덧셈 계산기


### 기능 요구사항
- [v] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
- [v] 로또 1장의 가격은 1000원이다.

### 실행 결과
```
구입금액을 입력해 주세요.
14000
14개를 구매했습니다.
[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

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

### 프로그래밍 요구 사항
- [v] 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
- [v] 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- [v] UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
- [v] indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.

- [v] 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
- [v] 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
- [v] 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다.
- [v] git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.

## 구현할 기능 목록

### View
- [v] InputView
- [v] 구입금액을 입력받는다
- "구입금액을 입력해 주세요."라는 안내문구를 출력한다.
- [v] 지난 주 당첨 번호를 입력받는다
- "지난 주 당첨 번호를 입력해 주세요."라는 안내문구를 출력한다.
- [v] Result View
- [v] 구입한 로또의 개수를 출력한다.
- [v] 구입한 로또의 번호를 출력한다.
- [v] 각각의 번호는 ","로 구분된다.
- [v] 로또 번호 Prefix는 '[', postfix는 ']' 를 사용한다.
- [v] 당첨 통계를 출력한다.
- [v] 3,4,5,6 개의 번호의 일치여부를 각각 출력한다.
- [v] 수익률을 표시한다.

### Domain
- [v] lotto market
- [v] 금액을 입력받는다
- [v] 숫자가 아니거나, 1000원보다 작거나, 음수이거나, 숫자가 아닐 경우 RuntimeExeption을 반환한다.
- [v] 구매 번호 만큼 로또를 생성한다.
- [v] lotto number generator
- 1부터 45까지의 숫자중 각기 다른 6개의 숫자를 발생시킨다.
- [v] Lotto
- [v] 6개의 각기 다른 번호를 가지고 있다.
- [v] 당첨 번호를 입력받아 매칭된 번호의 개수를 반환한다.
- [v] Lottos
- [v] lotto List의 콜렉션 기능을 담당한다.
- [v] WinningChecker
- [v] 당첨번호와 Lotto들을 입력받아 당첨여부를 판단한다
- [v] YieldCalculator
- [v] 당첨 금액을 계산한다.
- [v] 수익률을 계산한다.
- [v] WinningRank
- [v] Enum으로 만든다.
- [v] 복권의 당첨개수와, 그에 따른 금액을 기술한다.

### Service
- [v] lottoMarketService
- [v] lottoYieldService

### Controller
- [v] LottoController

### 설계 흐름
#### 로또 구매 프로세스
1. InputView: 구매 금액 입력.
2. LottoService:
1. LottoMarket을 호출하여 구매 금액 검증
2. 로또 생성.
2. 생성된 로또 리스트 반환.
4. ResultView
1. 로또 구매 개수와 번호 출력.

#### 당첨 및 수익률 계산 프로세스
1. InputView: 지난주 당첨 번호 입력.
2. LottoWinningStatisticsService
1. WinningChecker 호출하여 당첨 여부 판별.
2. 수익률 계산.
3. ResultView: 수익률 출력.





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

import lotto.core.LottoMarket
import lotto.presentation.InputView
import lotto.presentation.ResultView

object LottoController {
fun start() {
val purchaseAmount = InputView.inputPurchaseAmount()
val lottos = LottoMarket.purchase(purchaseAmount)
ResultView.printLottos(lottos)

val winningNumbers = InputView.inputWinningNumbers()
val winningStatistics = LottoWinningStatisticsService.start(lottos, winningNumbers)

ResultView.printWinningStatistics(winningStatistics)
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/lotto/application/LottoWinningStatisticsService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lotto.application

import lotto.core.LottoWinningStatistics
import lotto.core.Lottos
import lotto.core.WinningNumbers
import lotto.core.YieldCalculator

object LottoWinningStatisticsService {
fun start(
lottos: Lottos,
numbers: List<Int>,
): LottoWinningStatistics {
val winningNumbers = WinningNumbers(numbers)
val winningRankCount = lottos.countWinningRanks(winningNumbers)

return LottoWinningStatistics(winningRankCount, YieldCalculator.calculate(winningRankCount, lottos.size))
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/lotto/core/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package lotto.core

data class Lotto(val numbers: List<Int>)
25 changes: 25 additions & 0 deletions src/main/kotlin/lotto/core/LottoMarket.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lotto.core

import lotto.core.constant.LottoConstants

object LottoMarket {
fun purchase(purchaseAmount: String): Lottos {
val purchasableCount = calculatePurchasableCount(purchaseAmount)
val lottoList = issueLotto(purchasableCount)
return Lottos(lottoList)
}

private fun calculatePurchasableCount(purchaseAmount: String): Int {
val amount = purchaseAmount.toIntOrNull() ?: throw IllegalArgumentException("잘못된 금액이 입력되었습니다.")
return amount / LottoConstants.LOTTO_PRICE
}

private fun issueLotto(count: Int): List<Lotto> {
return List(count) { Lotto(generateNumbers()) }
}

private fun generateNumbers(): List<Int> =
(LottoConstants.LOTTO_NUMBER_MIN..LottoConstants.LOTTO_NUMBER_MAX).shuffled().take(
LottoConstants.LOTTO_NUMBER_COUNT,
).sorted()
}
3 changes: 3 additions & 0 deletions src/main/kotlin/lotto/core/LottoWinningStatistics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package lotto.core

data class LottoWinningStatistics(val winningRankCount: Map<WinningRank, Int>, val yieldRate: Float)
16 changes: 16 additions & 0 deletions src/main/kotlin/lotto/core/Lottos.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lotto.core

class Lottos(private val lottos: List<Lotto>) : List<Lotto> by lottos {
fun countWinningRanks(winningNumbers: WinningNumbers): Map<WinningRank, Int> {
val results = this.map { checkWinningState(it, winningNumbers) }

return results.groupingBy { it }.eachCount()
}

private fun checkWinningState(
lotto: Lotto,
winningNumbers: WinningNumbers,
): WinningRank {
return WinningRank.getWinningAmount(winningNumbers.countCommonNumbers(lotto))
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/lotto/core/WinningNumbers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package lotto.core

import lotto.core.constant.LottoConstants

data class WinningNumbers(val winningNumbers: List<Int>) {
Copy link

Choose a reason for hiding this comment

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

당첨번호들은 6개의 로또 번호를 가지는군요?
이미 구현해두신 Lotto와 비슷하게 느껴지시지 않으시나요? 😃

WinningNumbers를 따로 만들 필요 없이, 당첨 번호 == 로또 라고 볼 수도 있지 않을까요? 🤔

Copy link
Author

@bird798 bird798 Nov 26, 2024

Choose a reason for hiding this comment

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

네 만들고 난 다음에는 말씀하신것과 같이 기능적으로 Lotto와 같다고 생각했었습니다만,
로또는 발급의 대상으로 상품이라 인지하고, 당첨번호는 로또의 상태를 변화시키는 조건 같은 것이라 둘의 성질은 다른것이라고 생각했습니다.
또, 이후 보너스 넘버와 같은 추가 조건이 들어올 수 있다고 생각도 되었고, 그 경우 WinningNumbers를 Lotto와 같은 것으로 만들었을 때는 멤버가 달라지게 되서, 오히려 속성이 멀어지게 될거 같다는 생각이 듭니다.
물론 상속을 받아서 구현하는 방법도 있긴 합니다만, 그렇게 되면 생성자때문에 Data Class로는 만들기 어렵지 않을까 생각도 되고요.

Copy link

Choose a reason for hiding this comment

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

네 만들고 난 다음에는 말씀하신것과 같이 기능적으로 Lotto와 같다고 생각했었습니다만,
로또는 발급의 대상으로 상품이라 인지하고, 당첨번호는 로또의 상태를 변화시키는 조건 같은 것이라 둘의 성질은 다른것이라고 생각했습니다.

말씀하신 것처럼 자칫하면 어설픈 추상화가 될 수도 있지만 기능적인 것 뿐만이아니라 비즈니스 규칙조차 (1~45 범위의 로또 숫자를 6개만 가진다)도 완벽히 일치하기 때문에 저는 동일한 객체로 볼 수 있다고 생각해요. 😃

말씀하신 것처럼 추후 보너스 넘버와 같은 추가 조건이 생길 수도 있을거고 말씀하신 상속의 방식도 하나의 방법이 될 수도 있습니다만 단지 같은 상태를 가지게하기위해 혹은 중복을 제거하기위해 상속을 사용하는 것은 그렇게 권장되는 방식은 아닙니다. 😃

상속이 is a의 관계를 가진다면 has a 관계를 가지는 조합의 개념도 있는데요.

아래의 글을 참고해보시고 이러한 상황이 필요하다면 조합의 방식도 고려해보시면 어떨까 싶어요. 😃
상속보다는 조합(Composition)을 사용하자.

Copy link
Author

Choose a reason for hiding this comment

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

네. 말씀하신부분에 대해서 이해했습니다.

조합에 관련된 부분은 WinningNumbers Class는 Step3에서 보너스 넘버를 구현하면서 조합으로 변경해볼까 합니다.

init {
if (winningNumbers.size != LottoConstants.LOTTO_NUMBER_COUNT) {
throw IllegalArgumentException("당첨 번호의 숫자가 잘못되었습니다.")
}
}

fun countCommonNumbers(lotto: Lotto): Int {
return winningNumbers.filter { it in lotto.numbers }.size
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/lotto/core/WinningRank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lotto.core

enum class WinningRank(val winningCount: Int, val winningAmount: Int) {
RANK0(0, 0),
RANK4(3, 5_000),
RANK3(4, 50_000),
RANK2(5, 1_500_000),
RANK1(6, 2_000_000_000),
;

companion object {
private val map = entries.associateBy(WinningRank::winningCount)

fun getWinningAmount(winningCount: Int): WinningRank {
return map[winningCount] ?: RANK0
}
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/lotto/core/YieldCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package lotto.core

import lotto.core.constant.LottoConstants

object YieldCalculator {
fun calculate(
winningRankCount: Map<WinningRank, Int>,
lottoCount: Int,
): Float {
val totalBudget = lottoCount * LottoConstants.LOTTO_PRICE
val totalWinningAmount = winningRankCount.map { it.key.winningAmount * it.value }.sum()

return totalWinningAmount.toFloat() / totalBudget
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/lotto/core/constant/LottoConstants.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package lotto.core.constant

object LottoConstants {
const val LOTTO_NUMBER_COUNT = 6
const val LOTTO_NUMBER_MIN = 1
const val LOTTO_NUMBER_MAX = 45
const val LOTTO_PRICE = 1000
}
47 changes: 47 additions & 0 deletions src/main/kotlin/lotto/presentation/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package lotto.presentation

import lotto.core.constant.LottoConstants

object InputView {
fun inputPurchaseAmount(): String {
println(GUIDE_INPUT_PURCHASE_AMOUNT)

return readlnOrNull() ?: throw RuntimeException("잘못된 값이 입력되었습니다.")
}

fun inputWinningNumbers(): List<Int> {
println(GUIDE_INPUT_LAST_WINNING_NUMBER)

return splitNumbers(readlnOrNull() ?: throw RuntimeException("잘못된 값이 입력되었습니다."))
}

private fun splitNumbers(winningNumbers: String): List<Int> {
val numberList = winningNumbers.split(DELIMITER)
val numbers = transformNumbers(numberList)
checkInsideLottoNumber(numbers)
checkDuplicates(numbers)
return numbers
}

private fun transformNumbers(numberList: List<String>): List<Int> {
return numberList.map { number -> number.trim().toIntOrNull() ?: throw NumberFormatException("숫자가 아닙니다.") }
}

private fun checkDuplicates(list: List<Int>) {
if (list.size != list.toSet().size) {
throw IllegalArgumentException("중복된 숫자가 있습니다.")
}
}

private fun checkInsideLottoNumber(numbers: List<Int>) {
numbers.forEach {
if (it < LottoConstants.LOTTO_NUMBER_MIN || LottoConstants.LOTTO_NUMBER_MAX < it) {
throw IllegalArgumentException("로또 숫자의 범위를 넘어섰습니다.")
}
}
}

private const val GUIDE_INPUT_PURCHASE_AMOUNT = "구입금액을 입력해 주세요."
private const val GUIDE_INPUT_LAST_WINNING_NUMBER = "지난 주 당첨 번호를 입력해 주세요."
private const val DELIMITER = ","
}
57 changes: 57 additions & 0 deletions src/main/kotlin/lotto/presentation/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package lotto.presentation

import lotto.core.LottoWinningStatistics
import lotto.core.Lottos
import lotto.core.WinningRank

object ResultView {
fun printLottos(lottos: Lottos) {
val stringBuffer = StringBuffer()

stringBuffer.append(lottos.size)
stringBuffer.append(STR_PURCHASED_COUNT)
stringBuffer.append(STR_NEW_LINE)
lottos.forEach {
stringBuffer.append(it.numbers.joinToString(",", "[", "]"))
stringBuffer.append(STR_NEW_LINE)
}

println(stringBuffer.toString())
}

fun printWinningStatistics(winningStatistics: LottoWinningStatistics) {
val stringBuffer = StringBuffer()
stringBuffer.append(STR_WINNING_STATISTICS)
stringBuffer.append(STR_NEW_LINE)
stringBuffer.append(STR_SEPARATOR)
stringBuffer.append(STR_NEW_LINE)

winningStatistics.winningRankCount.filter { (it.key != WinningRank.RANK0) }
.map {
stringBuffer.append(it.key.winningCount)
stringBuffer.append(STR_MATCH)
stringBuffer.append(it.key.winningAmount)
stringBuffer.append(STR_AMOUNT)
stringBuffer.append(it.value)
stringBuffer.append(STR_COUNT)
stringBuffer.append(STR_NEW_LINE)
}

stringBuffer.append("총 수익률은 ")
stringBuffer.append(winningStatistics.yieldRate)
stringBuffer.append("입니다.")
stringBuffer.append(if (winningStatistics.yieldRate < 1) STR_PROFIT_IS_LOSS else STR_PROFIT_IS_GOOD)

println(stringBuffer.toString())
}

private const val STR_PURCHASED_COUNT = "개를 구매하였습니다."
private const val STR_NEW_LINE = "\n"
private const val STR_WINNING_STATISTICS = "당첨 통계"
private const val STR_SEPARATOR = "---------"
private const val STR_MATCH = "개 일치("
private const val STR_AMOUNT = "원) - "
private const val STR_COUNT = "개"
private const val STR_PROFIT_IS_LOSS = "기준이 1이기 때문에 결과적으로 손해라는 의미임"
private const val STR_PROFIT_IS_GOOD = "기준이 1이기 때문에 결과적으로 손해는 아니라는 의미임"
}
3 changes: 1 addition & 2 deletions src/main/kotlin/stringcalculator/core/Calculator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package stringcalculator.core

object Calculator {
fun sum(numbers: List<Number>): Int {
val sum = numbers.reduce { number1, number2 -> number1 + number2 }

val sum = numbers.reduce(Number::plus)
return sum.number
}
}
Loading