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

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0e74b99
Model, Test 추가
labyrinth30 May 7, 2024
9d0bf41
Purchase 클래스 추가
labyrinth30 May 7, 2024
eff3f62
test: Purchase 모델 테스트 코드 작성
cucumber99 May 8, 2024
f5c67b8
feat: Purchase 모델 구현
cucumber99 May 8, 2024
4e6dfcb
feat: 출력 뷰 구현
cucumber99 May 8, 2024
92c1bad
test: Tickets 모델 테스트
cucumber99 May 9, 2024
cb6823e
feat: Tickets 모델 구현
cucumber99 May 9, 2024
01e5bb0
refactor: Bonus 모델 수정
cucumber99 May 9, 2024
d99ee5e
test: Bonus 모델 테스트 수정
cucumber99 May 9, 2024
72df0ce
refactor: Lotto 모델 수정
cucumber99 May 9, 2024
62f533e
✅ Feat : Winning 테스트 구현
hyeminililo May 12, 2024
63ecd59
🎨 Winning 모델 구현
hyeminililo May 12, 2024
7081bb4
feat : Ranking 테스트 구현
hyeminililo May 12, 2024
71e9a19
refactor: 중복 검사 로직 수정
cucumber99 May 13, 2024
324ffb9
test: 테스트 코드 수정
cucumber99 May 13, 2024
2c3b332
test: Rank 모델 테스트
cucumber99 May 13, 2024
2583eb9
feat: Rank 모델 구현
cucumber99 May 13, 2024
e130820
Merge branch 'main' of https://github.com/hyeminililo/kotlin-lotto
hyeminililo May 13, 2024
7f2d4bf
test: 상금 및 수익률 로직 테스트
cucumber99 May 13, 2024
934dc9f
feat: 상금 및 수익률 모델 구현
cucumber99 May 13, 2024
f52766c
Merge branch 'main' of https://github.com/hyeminililo/kotlin-lotto
hyeminililo May 13, 2024
68c8f72
feat : Winning 클래스 삭제
hyeminililo May 13, 2024
a368a9a
프로그램 구현
cucumber99 May 13, 2024
7d4d9a6
Merge branch 'main' of https://github.com/cucumber99/kotlin-lotto
cucumber99 May 13, 2024
0b61d78
feat : 불필요한 테스트 코드 삭제
hyeminililo May 13, 2024
4ee0cda
Merge branch 'main' of https://github.com/cucumber99/kotlin-lotto
hyeminililo May 13, 2024
d5deecc
♻️ 1차 리팩토링
hyeminililo May 16, 2024
4232930
📝 Ticket 클래스 작성
hyeminililo May 16, 2024
83ce4a4
📝 TicketController 작성
hyeminililo May 17, 2024
2cd0a9e
🎨 구조 변경
hyeminililo May 17, 2024
ca64be2
🔥 Docs : Tickets 클래스 삭제
hyeminililo May 17, 2024
17ae544
♻️ Refactor : 2차 리팩토링
hyeminililo May 18, 2024
2fcb14b
🔥 Feat : 불필요한 테스트 코드 삭제
hyeminililo May 19, 2024
3c62fb7
📝 Test : ParameterizedTest 테스트 코드 구현
hyeminililo May 19, 2024
49fe198
✅ Test : 2차 리팩토링에 대한 테스트 성공
hyeminililo May 19, 2024
e4d7fc3
✅ Refactor : 3차 리팩토링
hyeminililo May 24, 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
83 changes: 83 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## 기능 구현 목록
### 입력
- 구입 금액을 입력받는다.
- [x] 1000원 단위로 입력
- [x] 1000으로 나누어 떨어지지 않는 경우 예외 처리
- [x] 자연수가 아닌 경우 예외 처리
- [x] 숫자가 아닌 경우 예외 처리
- 당첨 번호를 입력받는다.
- [x] 1~45 사이의 자연수 6개 입력
- [x] ','를 기준으로 구분
- [x] 공백이 존재하는 경우 예외 처리
- [x] 숫자가 아닌 경우 예외 처리
- [x] 중복된 값이 존재하는 경우 예외 처리
- 보너스 번호를 입력받는다.
- [x] 1~45 사이의 자연수 1개
- [x] 숫자가 아닌 경우 예외 처리
- [x] 당첨 번호와 중복되는 경우 예외 처리

### 출력
- 생성된 로또 번호를 출력한다.
- [x] 오름차순으로 출력
- 당첨 내역을 출력한다.
- [x] 상금과 개수를 출력한다.
- [x] 상금 출력시 ','를 천의 자리마다 배치한다.
- [x] '당첨 통계'와 결과 출력 사이에 '---'를 배치한다.
- 수익률을 출력한다.
- [x] 소수점 둘째 자리에서 반올림한다.

### 게임 로직
- 입력 받은 금액을 바탕으로 로또 개수를 계산한다.
- [x] 구입금액 / 1000(로또 1장의 가격)
- 계산된 개수만큼 로또 번호를 생성한다.
- [x] 1~45 사이의 중복되지 않는 자연수 6개 (pickUniqueNumberInRange)
- 로또 번호와 당첨 번호를 비교하여 순위를 계산한다.
- [x] 1등(6개), 2등(5개+보너스), 3등(5개), 4등(4개), 5등(3개)
- 수익률을 계산한다.
- [x] 수익률 = (당첨 금액/구매 금액)

### 예외상황
* 에러문구는 "[ERROR]"로 시작한다.
* 에러문구 출력 후, 그 부분부터 다시 입력받는다.
- [x] 당첨 번호, 보너스 번호를 입력 시, 숫자가 아닐 때
- [x] 금액이 1000원 단위가 아닐 때
- [x] 금액이 숫자가 아닐 때
- [x] 당첨 번호의 숫자 범위가 1~45 사이가 아닐 때
- [x] 당첨 번호를 입력할 때 쉼표 기준이 아닐 때
- [x] 당첨 번호에 동일한 수를 입력할 때

## Test 코드

### 입력 테스트 코드

- [x] 구입 금액에 해당하는 만큼 로또를 발행하는지 확인하는 테스트 코드
- [x] 에러 상황에서 다시 입력이 되는지 확인하는 코드

### 출력 테스트 코드

- [x] 동일한 수가 나오지 않는지
- [x] 번호가 일치하는지 확인하는 메소드
- [x] 수익률 테스트 코드

### Lotto 객체 관련 테스트 코드

- [x] 서로 다른 6개의 번호로 이루어진 로또 번호들이 잘 나오는지
- [x] 1~45 범위 안으로 로또 번호가 생성되는지

### Validation 테스트 코드

- [x] 당첨 번호, 보너스 번호를 입력할 때 숫자가 아닐 때
- [x] 금액이 1000원 단위가 아닐 때
- [x] 금액이 숫자가 아닐 때
- [x] 당첨 번호의 숫자 범위가 1~45 사이가 아닐 때
- [x] 당첨 번호를 입력할 때 쉼표 기준이 아닐 때
- [x] 당첨 번호에 동일한 수를 입력할 때


### 주의 사항
- 함수 또는 메소드의 길이가 10줄을 넘어가지 않도록 구현한다.
- 함수의 단일 책임 원칙을 준수한다.
- else 예약어를 사용하지 않는다. (early return)
- Enum 클래스를 사용한다.
- indent depth는 최대 1이 되도록 구현한다.
- 도메인 로직에 단위 테스트를 구현해야한다.
5 changes: 4 additions & 1 deletion src/main/kotlin/lotto/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package lotto

import lotto.controller.Controller

fun main() {
TODO("프로그램 구현")
val controller = Controller()
controller.start()
}
9 changes: 0 additions & 9 deletions src/main/kotlin/lotto/Lotto.kt

This file was deleted.

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

import lotto.model.*
import lotto.view.InputView
import lotto.view.OutputView
import lotto.view.OutputView.printPurchaseCountMessage
import lotto.view.OutputView.printTicketMessage

class Controller {
private val ticketController = TicketController()

// 시작하는 컨트롤러
fun start() {
val purchaseAmount = inputPurchaseAmount()
generateLottoTickets(purchaseAmount)
Copy link
Collaborator

Choose a reason for hiding this comment

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

제가 처음 코드를 읽을 때는
generateLottoTickets의 반환값이 없는 것에 의아함을 느끼고
메서드 내부를 들여다본 후, 결국 TicketController의 내부 구현까지 보고 나서야
해당 클래스에서 값을 생성하고 저장한다는 것을 알 수 있었습니다.
반환값을 받은 다음 이후 로직에서 값을 넘긴다든지, 생성과 저장 로직을 분리한다든지, 반환값을 사용하지 않으려면 메서드 이름을 [save-store]등으로 바꾼다든지... 하는 식으로 바꿔도 좋을 것 같아요.

사실 위에 강조표시한 방법 하나하나가 중요하다기보다는,
정말 중요한 것은
start 메서드를 위에서 아래로 쭉 읽었을 때 빠르게 이해되게끔 만드는 것 같아요!


val winningNumbers = inputWinningNumbers()
val bonusNumber = inputBonusNumber()
val rank = calculateRank(ticketController.tickets, winningNumbers, bonusNumber)
printStatistics(rank)
printEarningsRate(rank, purchaseAmount)
}

// 개수를 입력하는 메소드
private fun inputPurchaseAmount(): Int {
while (true) {
try {
OutputView.printAmountMessage()
return InputView.getInteger()
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
}

// 수량 만큼 로또 티켓들을 만드는 메소드
private fun generateLottoTickets(purchaseAmount: Int): List<List<Int>> {
val purchase = Purchase(purchaseAmount)
OutputView.printPurchaseCountMessage(purchase.calculatePurchaseCount())
// val tickets = Tickets(purchase)
val tickets = ticketController.generateTickets(purchase)
// val lottoTickets = ticketControler.generateTickets()
OutputView.printTicketMessage(ticketController.tickets)
return tickets
}

// 당첨 번호 입력하는 메소드
private fun inputWinningNumbers(): List<Int> {
while (true) {
try {
OutputView.printWinningNumberMessage()
return InputView.getWinningNumber()
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
}

// 보너스 번호 입력하는 메소드
private fun inputBonusNumber(): Int {
while (true) {
try {
OutputView.printBonusNumberMessage()
return InputView.getInteger()
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
}

// 순위 계산하는 메소드
private fun calculateRank(lottoTickets: List<List<Int>>, winningNumbers: List<Int>, bonusNumber: Int): Rank {
val lotto = Lotto(winningNumbers)
val bonus = Bonus(lotto, bonusNumber)
val rank = Rank(lotto, bonus)
rank.calculateRank(lottoTickets)
return rank
}

// 현재 랭크 출력하는 메소드
private fun printStatistics(rank: Rank) {
OutputView.printStatistics(rank.rankList)
}

// 수익률을 출력하는 메소드
private fun printEarningsRate(rank: Rank, purchaseAmount: Int) {
val prize = Prize(rank)
val prizeRate = prize.getRate(prize.getPrizeMoney(), purchaseAmount)
OutputView.printEarningsRate(prizeRate)
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/lotto/controller/TicketController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package lotto.controller

import lotto.model.Purchase
import lotto.model.Ticket

// 티켓을 관리하는 컨트롤러
class TicketController() {

var ticket = Ticket()
var tickets: MutableList<List<Int>> = mutableListOf()

// 개수만큼 티켓을 만드는 메소드
fun generateTickets(purchase: Purchase): List<List<Int>> {
repeat(purchase.calculatePurchaseCount()) {
val ticket = ticket.generateLottoNumber()
// ticket = ticket.generateLottoNumber()
addTicket(ticket)
}
return tickets
}

// tickets에 ticket을 추가하는 메소드
fun addTicket(ticket: List<Int>) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

요건 private으로 하는게 좋을 거 같네요. 접근 제어자는 private부터 시작해서 점점 넓혀 봅시다!
아니면 tickets.add를 바로 호출해도 될 것 같아요!

tickets.add(ticket)
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/lotto/model/Bonus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lotto.model

import lotto.util.Validator.validateBonusRange
import lotto.util.Validator.validateLottoBonusDuplicate

class Bonus (
private val lotto: Lotto,
Copy link
Collaborator

Choose a reason for hiding this comment

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

전체적으로 VO 방식을 전체적으로 잘 활용하신 것 같아요.
값을 클래스로 감싸고 생성자 레벨에서 검증하여 코드의 신뢰성을 보장하고, 규칙을 한 곳에서 관리하면
당장 관리해야 할 소스코드 파일의 수는 늘어나지만 여러 이점이 있죠.

요구사항에서 'LOTTO 클래스를 그대로 사용하라'라고 했는데
이 요구사항의 의도를 잘 파악하신 걸로 생각됩니다.

물론 모든 타입을 클래스로 감쌀 필욘 없지만, Lotto, Bonus는 게임 내에서 중요한 역할을 하는 요소이고
이러한 요소들에 대한 규칙은 한 곳에서 정의하여 관리하는 것이 좋기 때문에 좋은 결정 같습니다.

private val _bonusNumber: Int
) {

val bonusNumber: Int
get() = _bonusNumber

init {
validateBonusRange(_bonusNumber)
validateLottoBonusDuplicate(lotto.getWinningNumbers(), _bonusNumber)
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/model/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.model

import lotto.util.Validator.validateLottoDuplicate
import lotto.util.Validator.validateLottoRange
import lotto.util.Validator.validateLottoSize

class Lotto(private val numbers: List<Int>) {
init {
validateLottoSize(numbers)
validateLottoRange(numbers)
validateLottoDuplicate(numbers)
}

fun getWinningNumbers(): List<Int> {
return numbers
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/lotto/model/Prize.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lotto.model

class Prize(
private val rank: Rank
) {
companion object {
private const val PRIZE_1ST = 2_000_000_000
private const val PRIZE_2ND = 30_000_000
private const val PRIZE_3RD = 1_500_000
private const val PRIZE_4TH = 50_000
private const val PRIZE_5TH = 5_000

private const val PERCENT = 100
}

fun getPrizeMoney(): Int {
return rank.rankList[0] * PRIZE_1ST + rank.rankList[1] * PRIZE_2ND +
rank.rankList[2] * PRIZE_3RD + rank.rankList[3] * PRIZE_4TH + rank.rankList[4] * PRIZE_5TH
}

fun getRate(prizeMoney: Int, amount: Int): String {
val rate = (prizeMoney.toDouble() / amount.toDouble()) * PERCENT
return String.format("%.1f", rate)
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/model/Purchase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.model

import lotto.util.Validator.validateAmount

class Purchase(private val amount: Int) {
companion object {
private const val LOTTO_PRICE = 1000
}

init {
validateAmount(amount)
}

fun calculatePurchaseCount(): Int {
return amount / LOTTO_PRICE
}
}
49 changes: 49 additions & 0 deletions src/main/kotlin/lotto/model/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package lotto.model

class Rank(
private val lotto: Lotto,
private val bonus: Bonus,
) {
private val _rankList: MutableList<Int> = MutableList(6) { 0 }

val rankList: List<Int>
get() = _rankList

companion object {
private const val INDEX_1ST = 0
private const val INDEX_2ND = 1
private const val INDEX_3RD = 2
private const val INDEX_4TH = 3
private const val INDEX_5TH = 4
private const val INDEX_NONE = 5

private const val COUNT_SIX = 6
private const val COUNT_FIVE = 5
private const val COUNT_FOUR = 4
private const val COUNT_THREE = 3
}

fun calculateRank(tickets: List<List<Int>>) {
val winningNumbers = lotto.getWinningNumbers()
val bonusNumber = bonus.bonusNumber

tickets.forEach { ticket ->
val matchCount = ticket.count { it in winningNumbers }
val rankIndex = getRankIndex(matchCount, ticket.contains(bonusNumber))
_rankList[rankIndex]++
}
}

private fun getRankIndex(matchCount: Int, hasBonus: Boolean): Int {
if (matchCount == COUNT_SIX) return INDEX_1ST
if (matchCount == COUNT_FIVE) return countBonus(hasBonus)
if (matchCount == COUNT_FOUR) return INDEX_4TH
if (matchCount == COUNT_THREE) return INDEX_5TH
return INDEX_NONE
}

private fun countBonus(hasBonus: Boolean): Int {
if(hasBonus) return INDEX_2ND
return INDEX_3RD
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/lotto/model/Ticket.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lotto.model

import camp.nextstep.edu.missionutils.Randoms
import kotlin.random.Random
// Ticket 클래스 : 로또 숫자를 생성하는 메소드
class Ticket{
companion object {
const val MIN_NUMBER = 1
const val MAX_NUMBER = 45
const val LOTTO_SIZE = 6
}
// 로또 숫자를 생성하는 메소드 ( 리스트로 반환)
fun generateLottoNumber(): List<Int>{
return Randoms.pickUniqueNumbersInRange(MIN_NUMBER, MAX_NUMBER, LOTTO_SIZE).sorted()
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/lotto/util/ErrorMessage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package lotto.util

enum class ErrorMessage(private val message: String) {
NUMBER_NULL("입력값이 존재하지 않습니다."),
NUMBER_INTEGER("입력값은 숫자여야 합니다."),
NUMBER_NATURAL("입력값은 자연수여야 합니다."),
AMOUNT_UNIT("구매 금액은 1000원 단위로 입력되어야 합니다."),
LOTTO_RANGE("로또 번호의 숫자 범위는 1~45 사이여야 합니다."),
LOTTO_SIZE("로또 번호는 6개여야 합니다."),
LOTTO_DUPLICATE("당첨 번호는 각기 다른 수를 입력해야 합니다."),
BONUS_DUPLICATE("보너스 번호는 당첨 번호와 중복되어서는 안됩니다.");

fun getMessage(): String = "[ERROR] $message"
Copy link
Collaborator

Choose a reason for hiding this comment

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

이런 거 좋네요. 무작정 구현하다가 [ERROR]까지 밖에서 하드코딩으로 입력하게 하는 실수를 할 수도 있는데,
그러면 관리가 번거롭고 오타 낼 확률이 높죠.

이렇게 메서드 내부에서 [ERROR] String을 선언해서 이 메서드의 사용자가 특정한 형식보단 에러 메시지에 집중할 수 있게 됐네요!

}
Loading