-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: main
Are you sure you want to change the base?
Changes from 31 commits
0e74b99
9d0bf41
eff3f62
f5c67b8
4e6dfcb
92c1bad
cb6823e
01e5bb0
d99ee5e
72df0ce
62f533e
63ecd59
7081bb4
71e9a19
324ffb9
2c3b332
2583eb9
e130820
7f2d4bf
934dc9f
f52766c
68c8f72
a368a9a
7d4d9a6
0b61d78
4ee0cda
d5deecc
4232930
83ce4a4
2cd0a9e
ca64be2
17ae544
2fcb14b
3c62fb7
49fe198
e4d7fc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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이 되도록 구현한다. | ||
- 도메인 로직에 단위 테스트를 구현해야한다. |
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() | ||
} |
This file was deleted.
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) | ||
|
||
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) | ||
} | ||
} |
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>) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요건 private으로 하는게 좋을 거 같네요. 접근 제어자는 private부터 시작해서 점점 넓혀 봅시다! |
||
tickets.add(ticket) | ||
} | ||
} |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} |
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 | ||
} | ||
} |
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) | ||
} | ||
} |
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 | ||
} | ||
} |
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 | ||
} | ||
} |
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() | ||
} | ||
} |
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이런 거 좋네요. 무작정 구현하다가 이렇게 메서드 내부에서 [ERROR] String을 선언해서 이 메서드의 사용자가 특정한 형식보단 에러 메시지에 집중할 수 있게 됐네요! |
||
} |
There was a problem hiding this comment.
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 메서드를 위에서 아래로 쭉 읽었을 때 빠르게 이해되게끔
만드는 것 같아요!