From 7057a26605864992299d82f24248c39a2ce3bcc7 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Wed, 30 Oct 2024 23:13:23 +0900 Subject: [PATCH 01/17] =?UTF-8?q?docs:=20README=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로젝트 개요와 기능 요구 사항, 입출력 요구 사항을 작성 --- README.md | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/README.md b/README.md index 4fc0ae874..d40ec182c 100644 --- a/README.md +++ b/README.md @@ -1 +1,115 @@ # kotlin-lotto-precourse + +## 프로젝트 개요 + +이 프로젝트는 간단한 로또 발매기를 구현한 것입니다. 사용자가 입력한 금액에 따라 로또 번호를 발행하고, 당첨 번호를 입력받아 당첨 결과와 수익률을 계산합니다. + +--- + +## 기능 요구 사항 + +1. **금액 입력** + - 금액을 정수형으로 입력하지 않으면 예외 발생 + - 금액이 1000원으로 나누어떨어지지 않으면 예외 발생 +2. **금액에 따른 로또 번호 생성** + - 금액을 1000원으로 나눈만큼 랜덤으로 로또 번호를 생성 + - 하나의 로또 번호는 중복 없이 랜덤으로 6개 생성 + - 유효한 로또 번호는 1부터 45 사이의 숫자 +3. **로또 당첨 번호 입력** + - 로또 당첨 번호는 쉼표(,)로 구분된 6개의 숫자 + - 위의 양식을 지키지 않는 경우 예외 발생 + - 당첨 번호 중 1부터 45 사이의 숫자가 아닌 것이 있으면 예외 발생 +4. **보너스 번호 입력** + - 당첨 번호와 마찬가지로 1부터 45 사이의 숫자가 아닌 것이 있으면 예외 발생 +5. **결과 출력** + - 생성된 로또와 당첨 번호로 당첨 결과 출력 +6. **수익률 출력** + - `당첨 금액 / 로또 구매 금액 * 100`으로 수익률을 계산하여 출력 + +--- + +## 입출력 요구 사항 + +### 입력 + +- **구입 금액** : 1,000원 단위로 입력, 1,000으로 나누어 떨어지지 않는 경우 예외 처리 + ```plaintext + 14000 + ``` + +- **당첨 번호** : 쉼표(,)로 구분된 6개의 숫자 입력 + + ```plaintext + 1,2,3,4,5,6 + ``` + +- **보너스 번호** : 하나의 숫자 입력 + + ```plaintext + 7 + ``` + +### 출력 + +- **발행 로또 번호** : 구매한 로또 수량과 각 번호를 오름차순으로 출력 + + ```plaintext + 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] + ``` + +- **당첨 내역 및 수익률**: 각 당첨 등수별 일치 개수와 수익률 출력 + + ```plaintext + 3개 일치 (5,000원) - 1개 + 4개 일치 (50,000원) - 0개 + 5개 일치 (1,500,000원) - 0개 + 5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 + 6개 일치 (2,000,000,000원) - 0개 + 총 수익률은 62.5%입니다. + ``` + +- 에러 메시지: 유효하지 않은 값 입력 시 에러 메시지 출력 + + ```plaintext + [ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다. + ``` + +### 실행 예시 + +```plaintext +구입금액을 입력해 주세요. +8000 + +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] + +당첨 번호를 입력해 주세요. +1,2,3,4,5,6 + +보너스 번호를 입력해 주세요. +7 + +당첨 통계 +--- +3개 일치 (5,000원) - 1개 +4개 일치 (50,000원) - 0개 +5개 일치 (1,500,000원) - 0개 +5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 +6개 일치 (2,000,000,000원) - 0개 +총 수익률은 62.5%입니다. +``` \ No newline at end of file From 63fb5f3d01d5478cb298fba99f8f82592543f099 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Thu, 31 Oct 2024 23:52:47 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=80=ED=84=B0=20=EA=B8=88=EC=95=A1=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=B0=9B=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 16 +++++++- src/main/kotlin/lotto/Lotto.kt | 4 +- .../domain/exception/ExceptionMessages.kt | 11 ++++++ .../lotto/domain/validation/ValidateBudget.kt | 9 +++++ src/main/kotlin/lotto/ui/Ui.kt | 30 +++++++++++++++ .../domain/validation/ValidateBudgetKtTest.kt | 38 +++++++++++++++++++ 6 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt create mode 100644 src/main/kotlin/lotto/domain/validation/ValidateBudget.kt create mode 100644 src/main/kotlin/lotto/ui/Ui.kt create mode 100644 src/test/kotlin/lotto/domain/validation/ValidateBudgetKtTest.kt diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index 151821c9c..254d0d81b 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,5 +1,19 @@ package lotto +import lotto.ui.Ui + fun main() { - // TODO: 프로그램 구현 + val ui = Ui() + + val budget = keepRequestingForValidBudget(ui) +} + +private fun keepRequestingForValidBudget(ui: Ui): Int { + while (true) { + ui.requestBudget().onSuccess { budget -> + return budget + }.onFailure { exception -> + ui.displayExceptionMessage(exception.message) + } + } } diff --git a/src/main/kotlin/lotto/Lotto.kt b/src/main/kotlin/lotto/Lotto.kt index b97abc385..ce922f42e 100644 --- a/src/main/kotlin/lotto/Lotto.kt +++ b/src/main/kotlin/lotto/Lotto.kt @@ -5,5 +5,7 @@ class Lotto(private val numbers: List) { require(numbers.size == 6) { "[ERROR] 로또 번호는 6개여야 합니다." } } - // TODO: 추가 기능 구현 + companion object { + const val LOTTO_PRICE = 1_000 + } } diff --git a/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt new file mode 100644 index 000000000..20a95da25 --- /dev/null +++ b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt @@ -0,0 +1,11 @@ +package lotto.domain.exception + +import lotto.Lotto + +object ExceptionMessages { + const val DEFAULT_EXCEPTION_MESSAGE = "오류가 발생 했습니다. 다시 입력해 주세요." + const val BUDGET_NOT_DIVISIBLE_BY_LOTTO_PRICE = + "금액이 로또 금액(${Lotto.LOTTO_PRICE})으로 나누어 떨어지도록 입력해주세요." + const val BUDGET_NEEDS_TO_BE_BIGGER_THAN_ZERO = "금액은 0보다 커야합니다." + const val INVALID_NUMBER_FORMAT = "올바른 숫자 형식을 입력해 주세요." +} diff --git a/src/main/kotlin/lotto/domain/validation/ValidateBudget.kt b/src/main/kotlin/lotto/domain/validation/ValidateBudget.kt new file mode 100644 index 000000000..2ee70fd69 --- /dev/null +++ b/src/main/kotlin/lotto/domain/validation/ValidateBudget.kt @@ -0,0 +1,9 @@ +package lotto.domain.validation + +import lotto.Lotto.Companion.LOTTO_PRICE +import lotto.domain.exception.ExceptionMessages + +fun validateBudget(budget: Int) { + require(budget > 0) { ExceptionMessages.BUDGET_NEEDS_TO_BE_BIGGER_THAN_ZERO } + require(budget % LOTTO_PRICE == 0) { ExceptionMessages.BUDGET_NOT_DIVISIBLE_BY_LOTTO_PRICE } +} diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt new file mode 100644 index 000000000..9261edb26 --- /dev/null +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -0,0 +1,30 @@ +package lotto.ui + +import camp.nextstep.edu.missionutils.Console +import lotto.domain.exception.ExceptionMessages +import lotto.domain.validation.validateBudget + +class Ui { + fun requestBudget(): Result = runCatching { + displayEnterBudgetMessage() + val userInput = readInt().also { validateBudget(it) } + return@runCatching userInput + } + + fun displayExceptionMessage(message: String?): Unit = + System.err.println("$EXCEPTION_MESSAGE_HEADER ${message ?: ExceptionMessages.DEFAULT_EXCEPTION_MESSAGE}") + + private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) + + private fun readInt(): Int = runCatching { + Console.readLine().toInt() + }.getOrElse { + if (it is NumberFormatException) throw NumberFormatException(ExceptionMessages.INVALID_NUMBER_FORMAT) + throw it + } + + companion object { + private const val ENTER_BUDGET_MESSAGE = "구입금액을 입력해 주세요." + private const val EXCEPTION_MESSAGE_HEADER = "[ERROR]" + } +} diff --git a/src/test/kotlin/lotto/domain/validation/ValidateBudgetKtTest.kt b/src/test/kotlin/lotto/domain/validation/ValidateBudgetKtTest.kt new file mode 100644 index 000000000..58e3877b0 --- /dev/null +++ b/src/test/kotlin/lotto/domain/validation/ValidateBudgetKtTest.kt @@ -0,0 +1,38 @@ +package lotto.domain.validation + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class ValidateBudgetKtTest { + @Test + fun `1000원으로 나누어 떨어지는 금액 검증시 아무 일도 일어나지 않음`() { + // act, assert + assertDoesNotThrow { validateBudget(VALID_BUDGET) } + } + + @Test + fun `1000원으로 나누어 떨어지지 않는 금액 검증시 예외 발생`() { + // act, assert + assertThrows { validateBudget(INVALID_BUDGET) } + } + + @Test + fun `구입 금액으로 음수를 입력시 예외 발생`() { + // act, assert + assertThrows { validateBudget(NEGATIVE_BUDGET) } + } + + @Test + fun `구입 금액으로 0을 입력시 예외 발생`() { + // act, assert + assertThrows { validateBudget(ZERO_BUDGET) } + } + + companion object { + private const val VALID_BUDGET = 7_000 + private const val INVALID_BUDGET = 7_001 + private const val NEGATIVE_BUDGET = -2_000 + private const val ZERO_BUDGET = 0 + } +} From 9f24f326e2c84557744608dce94dcac7e12f7d51 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Fri, 1 Nov 2024 23:55:31 +0900 Subject: [PATCH 03/17] =?UTF-8?q?docs:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 금액 입력 예외 사항과 예외 처리, 수익률에 관란 요구 사항 추가 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d40ec182c..dc65158b4 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ 1. **금액 입력** - 금액을 정수형으로 입력하지 않으면 예외 발생 - 금액이 1000원으로 나누어떨어지지 않으면 예외 발생 + - 금액이 0 이하일 경우 예외 발생 2. **금액에 따른 로또 번호 생성** - 금액을 1000원으로 나눈만큼 랜덤으로 로또 번호를 생성 - 하나의 로또 번호는 중복 없이 랜덤으로 6개 생성 @@ -25,6 +26,10 @@ - 생성된 로또와 당첨 번호로 당첨 결과 출력 6. **수익률 출력** - `당첨 금액 / 로또 구매 금액 * 100`으로 수익률을 계산하여 출력 + - 수익률은 소수점 둘째 자리에서 반올림 +7. **예외 처리** + - 사용자가 잘못된 값을 입력시 알맞은 예외를 발생시키고 에러 메시지 출력 후 입력을 다시 받음 + - 에러 메시지는 "[ERROR]"로 시작 --- From f2293a8c873b656477ac13969ac97406d55fedd6 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Sat, 2 Nov 2024 23:49:07 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EC=98=AC=EB=B0=94=EB=A5=B8=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EA=B0=92=EC=9D=B4=20=EB=82=98=EC=98=AC=20?= =?UTF-8?q?=EB=95=8C=EA=B9=8C=EC=A7=80=20=EB=B0=98=EB=B3=B5=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=ED=98=B8=EC=B6=9C=ED=95=98=EB=8A=94=20util=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 13 ++---- .../lotto/util/KeepCallingForSuccessResult.kt | 14 ++++++ .../util/KeepCallingForSuccessResultKtTest.kt | 43 +++++++++++++++++++ 3 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt create mode 100644 src/test/kotlin/lotto/util/KeepCallingForSuccessResultKtTest.kt diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index 254d0d81b..9a464f1e6 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,19 +1,12 @@ package lotto import lotto.ui.Ui +import lotto.util.keepCallingForSuccessResult fun main() { val ui = Ui() - val budget = keepRequestingForValidBudget(ui) + val budget = keepCallingForSuccessResult(onFailure = ui::handleFailure, actionToCall = ui::requestBudget) } -private fun keepRequestingForValidBudget(ui: Ui): Int { - while (true) { - ui.requestBudget().onSuccess { budget -> - return budget - }.onFailure { exception -> - ui.displayExceptionMessage(exception.message) - } - } -} +fun Ui.handleFailure(exception: Throwable): Unit = displayExceptionMessage(exception.message) diff --git a/src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt b/src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt new file mode 100644 index 000000000..eb2755ad2 --- /dev/null +++ b/src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt @@ -0,0 +1,14 @@ +package lotto.util + +fun keepCallingForSuccessResult( + onFailure: ((Throwable) -> Unit)? = null, + actionToCall: () -> Result, +): R { + while (true) { + actionToCall().onSuccess { value -> + return value + }.onFailure { exception -> + onFailure?.invoke(exception) + } + } +} diff --git a/src/test/kotlin/lotto/util/KeepCallingForSuccessResultKtTest.kt b/src/test/kotlin/lotto/util/KeepCallingForSuccessResultKtTest.kt new file mode 100644 index 000000000..08c57df09 --- /dev/null +++ b/src/test/kotlin/lotto/util/KeepCallingForSuccessResultKtTest.kt @@ -0,0 +1,43 @@ +package lotto.util + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + + +class KeepCallingForSuccessResultKtTest { + @Test + fun `Result가 Success일 경우 정상적으로 값을 반환`() { + // arrange + val action = { Result.success(SUCCESS_VALUE) } + + // act + val returnedValue = keepCallingForSuccessResult(actionToCall = action) + + // assert + assertThat(returnedValue).isEqualTo(SUCCESS_VALUE) + } + + @Test + fun `Result가 Failure일 경우 Success가 반환될 때까지 actionToCall을 호출`() { + // arrange + var onFailureCallCounter = 0 + val action = action@{ + if (onFailureCallCounter == MAX_CALL_COUNTER) return@action Result.success(onFailureCallCounter) + return@action Result.failure(IllegalArgumentException()) + } + + // act + val returnedValue = keepCallingForSuccessResult( + onFailure = { ++onFailureCallCounter }, + actionToCall = action + ) + + // assert + assertThat(returnedValue).isEqualTo(MAX_CALL_COUNTER) + } + + companion object { + private const val SUCCESS_VALUE = 123 + private const val MAX_CALL_COUNTER = 100 + } +} From f3bc6ee174aaf7685af17d7521f6b0825508799e Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Sun, 3 Nov 2024 17:58:50 +0900 Subject: [PATCH 05/17] =?UTF-8?q?fix:=20NoSuchElementException=EC=9D=98=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=EB=8A=94=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NoSuchElementException의 경우 실제 프로그램 실행시 일어나지 않고 테스트 코드에서는 해당 예외가 테스트가 성공적으로 종료되기 위해 필요함 --- src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt b/src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt index eb2755ad2..4562bfc34 100644 --- a/src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt +++ b/src/main/kotlin/lotto/util/KeepCallingForSuccessResult.kt @@ -8,6 +8,7 @@ fun keepCallingForSuccessResult( actionToCall().onSuccess { value -> return value }.onFailure { exception -> + if (exception is NoSuchElementException) throw exception onFailure?.invoke(exception) } } From 81bd6aa096d26a0fac0c067dfc651ab472c077aa Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Sun, 3 Nov 2024 21:16:59 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=9C=EB=A0=A5=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EC=97=90=EC=84=9C=20Throwable=EC=9D=84=20=EB=B0=9B?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 4 +--- src/main/kotlin/lotto/ui/Ui.kt | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index 9a464f1e6..15d708655 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -6,7 +6,5 @@ import lotto.util.keepCallingForSuccessResult fun main() { val ui = Ui() - val budget = keepCallingForSuccessResult(onFailure = ui::handleFailure, actionToCall = ui::requestBudget) + val budget = keepCallingForSuccessResult(onFailure = ui::displayExceptionMessage, actionToCall = ui::requestBudget) } - -fun Ui.handleFailure(exception: Throwable): Unit = displayExceptionMessage(exception.message) diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt index 9261edb26..2486ae926 100644 --- a/src/main/kotlin/lotto/ui/Ui.kt +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -11,8 +11,10 @@ class Ui { return@runCatching userInput } - fun displayExceptionMessage(message: String?): Unit = - System.err.println("$EXCEPTION_MESSAGE_HEADER ${message ?: ExceptionMessages.DEFAULT_EXCEPTION_MESSAGE}") + fun displayExceptionMessage(exception: Throwable) { + val message = exception.message ?: ExceptionMessages.DEFAULT_EXCEPTION_MESSAGE + System.err.println("$EXCEPTION_MESSAGE_HEADER $message") + } private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) From 2a7f6a85229f3a318a94748a4b482376fd5c5919 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Sun, 3 Nov 2024 21:22:41 +0900 Subject: [PATCH 07/17] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=EB=A5=BC=20stdout=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/ui/Ui.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt index 2486ae926..ff2b38abd 100644 --- a/src/main/kotlin/lotto/ui/Ui.kt +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -13,7 +13,7 @@ class Ui { fun displayExceptionMessage(exception: Throwable) { val message = exception.message ?: ExceptionMessages.DEFAULT_EXCEPTION_MESSAGE - System.err.println("$EXCEPTION_MESSAGE_HEADER $message") + println("$EXCEPTION_MESSAGE_HEADER $message") } private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) From dadc342e636eca387e7a9066b1e3fdb73d480c49 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 01:35:21 +0900 Subject: [PATCH 08/17] =?UTF-8?q?docs:=20=EB=A1=9C=EB=98=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=97=90=EC=84=9C=20=EC=B6=9C=EB=A0=A5=EC=97=90=20?= =?UTF-8?q?=EA=B4=80=ED=95=9C=20=EB=82=B4=EC=9A=A9=20=EB=B0=8F=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dc65158b4..dbb688795 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,12 @@ - 금액을 정수형으로 입력하지 않으면 예외 발생 - 금액이 1000원으로 나누어떨어지지 않으면 예외 발생 - 금액이 0 이하일 경우 예외 발생 -2. **금액에 따른 로또 번호 생성** +2. **금액에 따른 로또 번호 생성 및 출력** - 금액을 1000원으로 나눈만큼 랜덤으로 로또 번호를 생성 + - 몇개의 로또를 구매했는지와 각각의 로또 번호를 오름차순으로 정렬하여 출력 - 하나의 로또 번호는 중복 없이 랜덤으로 6개 생성 - 유효한 로또 번호는 1부터 45 사이의 숫자 + - 유효 범위 외의 숫자가 존재하거나 6개가 아닌 경우 예외 발생 3. **로또 당첨 번호 입력** - 로또 당첨 번호는 쉼표(,)로 구분된 6개의 숫자 - 위의 양식을 지키지 않는 경우 예외 발생 From 54d1611e9cf932e72be8ff85b8d18ac2ec419b02 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 18:55:05 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=EC=9E=85=EB=A0=A5=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EA=B8=88=EC=95=A1=EC=97=90=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A7=8C=ED=81=BC=20=EB=A1=9C=EB=98=90?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=20=ED=9B=84=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 9 ++++ src/main/kotlin/lotto/Lotto.kt | 12 +++++- .../data/random/LottoNumberGeneratorImpl.kt | 10 +++++ .../domain/exception/ExceptionMessages.kt | 6 +++ .../lotto/domain/factory/LottoFactory.kt | 13 ++++++ .../domain/random/LottoNumberGenerator.kt | 5 +++ src/main/kotlin/lotto/ui/Ui.kt | 16 ++++++++ src/test/kotlin/lotto/LottoTest.kt | 41 ++++++++++++++++++- .../data/random/FakeLottoNumberGenerator.kt | 10 +++++ .../random/LottoNumberGeneratorImplKtTest.kt | 24 +++++++++++ .../lotto/domain/factory/LottoFactoryTest.kt | 40 ++++++++++++++++++ 11 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt create mode 100644 src/main/kotlin/lotto/domain/factory/LottoFactory.kt create mode 100644 src/main/kotlin/lotto/domain/random/LottoNumberGenerator.kt create mode 100644 src/test/kotlin/lotto/data/random/FakeLottoNumberGenerator.kt create mode 100644 src/test/kotlin/lotto/data/random/LottoNumberGeneratorImplKtTest.kt create mode 100644 src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index 15d708655..42f7bc726 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,5 +1,7 @@ package lotto +import lotto.data.random.pickLottoNumbers +import lotto.domain.factory.LottoFactory import lotto.ui.Ui import lotto.util.keepCallingForSuccessResult @@ -7,4 +9,11 @@ fun main() { val ui = Ui() val budget = keepCallingForSuccessResult(onFailure = ui::displayExceptionMessage, actionToCall = ui::requestBudget) + + val amount = budget / Lotto.LOTTO_PRICE + ui.displayAmount(amount) + + val lottoFactory = LottoFactory(lottoNumberGenerator = ::pickLottoNumbers) + val lottoes = lottoFactory.createLottoes(amount) + ui.displayLottoes(lottoes) } diff --git a/src/main/kotlin/lotto/Lotto.kt b/src/main/kotlin/lotto/Lotto.kt index ce922f42e..75dc09fcd 100644 --- a/src/main/kotlin/lotto/Lotto.kt +++ b/src/main/kotlin/lotto/Lotto.kt @@ -1,11 +1,21 @@ package lotto +import lotto.domain.exception.ExceptionMessages + class Lotto(private val numbers: List) { init { - require(numbers.size == 6) { "[ERROR] 로또 번호는 6개여야 합니다." } + require(numbers.size == VALID_LOTTO_NUMBER_LENGTH) { ExceptionMessages.INVALID_LOTTO_NUMBER_LENGTH } + require(numbers.distinct().size == numbers.size) { ExceptionMessages.DUPLICATE_NUMBER_EXISTS } + require(numbers.all { number -> number in VALID_LOTTO_NUMBER_RANGE }) { + ExceptionMessages.NUMBER_OUT_OF_VALID_LOTTO_RANGE_EXISTS + } } + override fun toString(): String = numbers.sorted().toString() + companion object { const val LOTTO_PRICE = 1_000 + const val VALID_LOTTO_NUMBER_LENGTH = 6 + val VALID_LOTTO_NUMBER_RANGE = 1..45 } } diff --git a/src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt b/src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt new file mode 100644 index 000000000..e25abd538 --- /dev/null +++ b/src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt @@ -0,0 +1,10 @@ +package lotto.data.random + +import camp.nextstep.edu.missionutils.Randoms +import lotto.Lotto + +fun pickLottoNumbers(): List = Randoms.pickUniqueNumbersInRange( + Lotto.VALID_LOTTO_NUMBER_RANGE.first, + Lotto.VALID_LOTTO_NUMBER_RANGE.last, + Lotto.VALID_LOTTO_NUMBER_LENGTH +) diff --git a/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt index 20a95da25..4ce46b5fe 100644 --- a/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt +++ b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt @@ -1,6 +1,8 @@ package lotto.domain.exception import lotto.Lotto +import lotto.Lotto.Companion.VALID_LOTTO_NUMBER_LENGTH +import lotto.Lotto.Companion.VALID_LOTTO_NUMBER_RANGE object ExceptionMessages { const val DEFAULT_EXCEPTION_MESSAGE = "오류가 발생 했습니다. 다시 입력해 주세요." @@ -8,4 +10,8 @@ object ExceptionMessages { "금액이 로또 금액(${Lotto.LOTTO_PRICE})으로 나누어 떨어지도록 입력해주세요." const val BUDGET_NEEDS_TO_BE_BIGGER_THAN_ZERO = "금액은 0보다 커야합니다." const val INVALID_NUMBER_FORMAT = "올바른 숫자 형식을 입력해 주세요." + const val INVALID_LOTTO_NUMBER_LENGTH = "로또 번호는 ${VALID_LOTTO_NUMBER_LENGTH}개여야 합니다." + val NUMBER_OUT_OF_VALID_LOTTO_RANGE_EXISTS = + "로또 번호는 ${VALID_LOTTO_NUMBER_RANGE.first}와 ${VALID_LOTTO_NUMBER_RANGE.last} 사이의 값이어야 합니다." + const val DUPLICATE_NUMBER_EXISTS = "로또 번호에 중복이 없어야 합니다." } diff --git a/src/main/kotlin/lotto/domain/factory/LottoFactory.kt b/src/main/kotlin/lotto/domain/factory/LottoFactory.kt new file mode 100644 index 000000000..913748f76 --- /dev/null +++ b/src/main/kotlin/lotto/domain/factory/LottoFactory.kt @@ -0,0 +1,13 @@ +package lotto.domain.factory + +import lotto.Lotto +import lotto.domain.random.LottoNumberGenerator + +class LottoFactory( + private val lottoNumberGenerator: LottoNumberGenerator +) { + fun createLottoes(amount: Int): List = List(amount) { + val lottoNumbers = lottoNumberGenerator.pickLottoNumbers() + Lotto(lottoNumbers) + } +} diff --git a/src/main/kotlin/lotto/domain/random/LottoNumberGenerator.kt b/src/main/kotlin/lotto/domain/random/LottoNumberGenerator.kt new file mode 100644 index 000000000..1d204b1d9 --- /dev/null +++ b/src/main/kotlin/lotto/domain/random/LottoNumberGenerator.kt @@ -0,0 +1,5 @@ +package lotto.domain.random + +fun interface LottoNumberGenerator { + fun pickLottoNumbers(): List +} diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt index ff2b38abd..d49db5b9a 100644 --- a/src/main/kotlin/lotto/ui/Ui.kt +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -1,6 +1,7 @@ package lotto.ui import camp.nextstep.edu.missionutils.Console +import lotto.Lotto import lotto.domain.exception.ExceptionMessages import lotto.domain.validation.validateBudget @@ -16,6 +17,19 @@ class Ui { println("$EXCEPTION_MESSAGE_HEADER $message") } + fun displayAmount(amount: Int): Unit = println("${NEW_LINE_FEED}${amount}${AMOUNT_MESSAGE}") + + fun displayLottoes(lottoes: List) { + val stringBuilder = StringBuilder() + + for (lotto in lottoes) { + stringBuilder.append(lotto.toString()) + stringBuilder.append(NEW_LINE_FEED) + } + + println(stringBuilder.toString()) + } + private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) private fun readInt(): Int = runCatching { @@ -28,5 +42,7 @@ class Ui { companion object { private const val ENTER_BUDGET_MESSAGE = "구입금액을 입력해 주세요." private const val EXCEPTION_MESSAGE_HEADER = "[ERROR]" + private const val AMOUNT_MESSAGE = "개를 구매했습니다." + private const val NEW_LINE_FEED = '\n' } } diff --git a/src/test/kotlin/lotto/LottoTest.kt b/src/test/kotlin/lotto/LottoTest.kt index 122fae572..17417fecc 100644 --- a/src/test/kotlin/lotto/LottoTest.kt +++ b/src/test/kotlin/lotto/LottoTest.kt @@ -1,23 +1,60 @@ package lotto +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows + class LottoTest { + @Test + fun `로또 번호의 개수가 6개이고 모두 유효한 범위 내에 있을 경우 아무 일도 일어나지 않음`() { + // act, assert + assertDoesNotThrow { Lotto(listOf(1, 2, 3, 4, 5, 6)) } + } + @Test fun `로또 번호의 개수가 6개가 넘어가면 예외가 발생한다`() { + // act, assert assertThrows { Lotto(listOf(1, 2, 3, 4, 5, 6, 7)) } } - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 + @Test + fun `로또 번호의 개수가 6개 미만일 경우 예외 발생`() { + // act, assert + assertThrows { + Lotto(listOf(1, 2, 3, 4, 5)) + } + } + @Test fun `로또 번호에 중복된 숫자가 있으면 예외가 발생한다`() { + // act, assert assertThrows { Lotto(listOf(1, 2, 3, 4, 5, 5)) } } - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + @Test + fun `유효한 로또 번호 범위 외의 숫자가 존재할 경우 예외 발생`() { + // act, assert + assertThrows { + Lotto(listOf(1, 2, 3, 4, 5, 46)) + } + } + + @Test + fun `toString은 항상 오름차순의 로또 번호 문자열을 반환`() { + // arrange + val lotto = Lotto(listOf(6, 5, 4, 3, 2, 1)) + + // act + val result = lotto.toString() + + // assert + val expectedResult = "[1, 2, 3, 4, 5, 6]" + assertThat(result).isEqualTo(expectedResult) + } } diff --git a/src/test/kotlin/lotto/data/random/FakeLottoNumberGenerator.kt b/src/test/kotlin/lotto/data/random/FakeLottoNumberGenerator.kt new file mode 100644 index 000000000..63156205d --- /dev/null +++ b/src/test/kotlin/lotto/data/random/FakeLottoNumberGenerator.kt @@ -0,0 +1,10 @@ +package lotto.data.random + +import lotto.domain.random.LottoNumberGenerator + +class FakeLottoNumberGenerator : LottoNumberGenerator { + var nextLottoNumbers: MutableList> = mutableListOf() + val maxAmount get() = nextLottoNumbers.size + + override fun pickLottoNumbers(): List = nextLottoNumbers.removeFirst() +} diff --git a/src/test/kotlin/lotto/data/random/LottoNumberGeneratorImplKtTest.kt b/src/test/kotlin/lotto/data/random/LottoNumberGeneratorImplKtTest.kt new file mode 100644 index 000000000..2f500b2b3 --- /dev/null +++ b/src/test/kotlin/lotto/data/random/LottoNumberGeneratorImplKtTest.kt @@ -0,0 +1,24 @@ +package lotto.data.random + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class LottoNumberGeneratorImplKtTest { + @Test + fun `pickLottoNumbers는 6개의 숫자를 생성`() { + // act + val lottoNumbers = pickLottoNumbers() + + // assert + assertThat(lottoNumbers.size).isEqualTo(6) + } + + @Test + fun `pickLottoNumbers는 중복되지 않는 숫자를 생성`() { + // act + val lottoNumbers = pickLottoNumbers() + + // assert + assertThat(lottoNumbers.size).isEqualTo(lottoNumbers.distinct().size) + } +} diff --git a/src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt b/src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt new file mode 100644 index 000000000..f0906d2bf --- /dev/null +++ b/src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt @@ -0,0 +1,40 @@ +package lotto.domain.factory + +import lotto.data.random.FakeLottoNumberGenerator +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + + +class LottoFactoryTest { + private lateinit var sut: LottoFactory + + @Test + fun `LottoFactory에 주입된 LottoNumberGenerator에서 생성된 값으로 로또 생성`() { + // arrange + val lottoNumberGenerator = FakeLottoNumberGenerator().apply { + nextLottoNumbers = mutableListOf( + listOf(1, 2, 3, 4, 5, 6), + listOf(2, 1, 3, 4, 5, 6), + listOf(6, 5, 4, 3, 2, 1), + listOf(23, 4, 33, 12, 20, 44), + listOf(29, 39, 8, 17, 22, 35) + ) + } + sut = LottoFactory(lottoNumberGenerator) + + // act + val lottoes = sut.createLottoes(lottoNumberGenerator.maxAmount) + + // assert + val expectedResults = listOf( + "[1, 2, 3, 4, 5, 6]", + "[1, 2, 3, 4, 5, 6]", + "[1, 2, 3, 4, 5, 6]", + "[4, 12, 20, 23, 33, 44]", + "[8, 17, 22, 29, 35, 39]" + ) + lottoes.zip(expectedResults).forEach { (lotto, expectedResult) -> + assertThat(lotto.toString()).isEqualTo(expectedResult) + } + } +} From b4ed4c74ec330879b8687059d768b864ffb9e09d Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 18:58:27 +0900 Subject: [PATCH 10/17] =?UTF-8?q?refactor:=20Lotto=EC=99=80=20LottoFactory?= =?UTF-8?q?=EC=9D=98=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 3 ++- .../kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt | 2 +- src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt | 6 +++--- src/main/kotlin/lotto/{ => domain/model}/Lotto.kt | 2 +- .../kotlin/lotto/domain/{ => model}/factory/LottoFactory.kt | 4 ++-- src/main/kotlin/lotto/domain/validation/ValidateBudget.kt | 2 +- src/main/kotlin/lotto/ui/Ui.kt | 2 +- src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt | 1 + src/test/kotlin/lotto/{ => domain/model}/LottoTest.kt | 2 +- 9 files changed, 13 insertions(+), 11 deletions(-) rename src/main/kotlin/lotto/{ => domain/model}/Lotto.kt (96%) rename src/main/kotlin/lotto/domain/{ => model}/factory/LottoFactory.kt (81%) rename src/test/kotlin/lotto/{ => domain/model}/LottoTest.kt (98%) diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index 42f7bc726..4e7da7edd 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,7 +1,8 @@ package lotto import lotto.data.random.pickLottoNumbers -import lotto.domain.factory.LottoFactory +import lotto.domain.model.factory.LottoFactory +import lotto.domain.model.Lotto import lotto.ui.Ui import lotto.util.keepCallingForSuccessResult diff --git a/src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt b/src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt index e25abd538..0af6e69c7 100644 --- a/src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt +++ b/src/main/kotlin/lotto/data/random/LottoNumberGeneratorImpl.kt @@ -1,7 +1,7 @@ package lotto.data.random import camp.nextstep.edu.missionutils.Randoms -import lotto.Lotto +import lotto.domain.model.Lotto fun pickLottoNumbers(): List = Randoms.pickUniqueNumbersInRange( Lotto.VALID_LOTTO_NUMBER_RANGE.first, diff --git a/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt index 4ce46b5fe..f35c5b037 100644 --- a/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt +++ b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt @@ -1,8 +1,8 @@ package lotto.domain.exception -import lotto.Lotto -import lotto.Lotto.Companion.VALID_LOTTO_NUMBER_LENGTH -import lotto.Lotto.Companion.VALID_LOTTO_NUMBER_RANGE +import lotto.domain.model.Lotto +import lotto.domain.model.Lotto.Companion.VALID_LOTTO_NUMBER_LENGTH +import lotto.domain.model.Lotto.Companion.VALID_LOTTO_NUMBER_RANGE object ExceptionMessages { const val DEFAULT_EXCEPTION_MESSAGE = "오류가 발생 했습니다. 다시 입력해 주세요." diff --git a/src/main/kotlin/lotto/Lotto.kt b/src/main/kotlin/lotto/domain/model/Lotto.kt similarity index 96% rename from src/main/kotlin/lotto/Lotto.kt rename to src/main/kotlin/lotto/domain/model/Lotto.kt index 75dc09fcd..f4521e451 100644 --- a/src/main/kotlin/lotto/Lotto.kt +++ b/src/main/kotlin/lotto/domain/model/Lotto.kt @@ -1,4 +1,4 @@ -package lotto +package lotto.domain.model import lotto.domain.exception.ExceptionMessages diff --git a/src/main/kotlin/lotto/domain/factory/LottoFactory.kt b/src/main/kotlin/lotto/domain/model/factory/LottoFactory.kt similarity index 81% rename from src/main/kotlin/lotto/domain/factory/LottoFactory.kt rename to src/main/kotlin/lotto/domain/model/factory/LottoFactory.kt index 913748f76..d3eb09d97 100644 --- a/src/main/kotlin/lotto/domain/factory/LottoFactory.kt +++ b/src/main/kotlin/lotto/domain/model/factory/LottoFactory.kt @@ -1,6 +1,6 @@ -package lotto.domain.factory +package lotto.domain.model.factory -import lotto.Lotto +import lotto.domain.model.Lotto import lotto.domain.random.LottoNumberGenerator class LottoFactory( diff --git a/src/main/kotlin/lotto/domain/validation/ValidateBudget.kt b/src/main/kotlin/lotto/domain/validation/ValidateBudget.kt index 2ee70fd69..7e6c97396 100644 --- a/src/main/kotlin/lotto/domain/validation/ValidateBudget.kt +++ b/src/main/kotlin/lotto/domain/validation/ValidateBudget.kt @@ -1,6 +1,6 @@ package lotto.domain.validation -import lotto.Lotto.Companion.LOTTO_PRICE +import lotto.domain.model.Lotto.Companion.LOTTO_PRICE import lotto.domain.exception.ExceptionMessages fun validateBudget(budget: Int) { diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt index d49db5b9a..a3af7fa03 100644 --- a/src/main/kotlin/lotto/ui/Ui.kt +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -1,8 +1,8 @@ package lotto.ui import camp.nextstep.edu.missionutils.Console -import lotto.Lotto import lotto.domain.exception.ExceptionMessages +import lotto.domain.model.Lotto import lotto.domain.validation.validateBudget class Ui { diff --git a/src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt b/src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt index f0906d2bf..1b2081950 100644 --- a/src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt +++ b/src/test/kotlin/lotto/domain/factory/LottoFactoryTest.kt @@ -1,6 +1,7 @@ package lotto.domain.factory import lotto.data.random.FakeLottoNumberGenerator +import lotto.domain.model.factory.LottoFactory import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/lotto/LottoTest.kt b/src/test/kotlin/lotto/domain/model/LottoTest.kt similarity index 98% rename from src/test/kotlin/lotto/LottoTest.kt rename to src/test/kotlin/lotto/domain/model/LottoTest.kt index 17417fecc..8288f094c 100644 --- a/src/test/kotlin/lotto/LottoTest.kt +++ b/src/test/kotlin/lotto/domain/model/LottoTest.kt @@ -1,4 +1,4 @@ -package lotto +package lotto.domain.model import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test From 19d2412a39d3684e0b9ea0351967befff8701b7b Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 20:11:46 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20=EB=8B=B9=EC=B2=A8=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9E=85=EB=A0=A5=EC=9D=84=20=EB=B0=9B=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 5 +- .../validation/ValidateWinningNumbers.kt | 8 +++ src/main/kotlin/lotto/ui/Ui.kt | 18 +++++++ .../ValidateWinningNumbersKtTest.kt | 50 +++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/lotto/domain/validation/ValidateWinningNumbers.kt create mode 100644 src/test/kotlin/lotto/domain/validation/ValidateWinningNumbersKtTest.kt diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index 4e7da7edd..af15e6550 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,8 +1,8 @@ package lotto import lotto.data.random.pickLottoNumbers -import lotto.domain.model.factory.LottoFactory import lotto.domain.model.Lotto +import lotto.domain.model.factory.LottoFactory import lotto.ui.Ui import lotto.util.keepCallingForSuccessResult @@ -17,4 +17,7 @@ fun main() { val lottoFactory = LottoFactory(lottoNumberGenerator = ::pickLottoNumbers) val lottoes = lottoFactory.createLottoes(amount) ui.displayLottoes(lottoes) + + val winningNumbers = + keepCallingForSuccessResult(onFailure = ui::displayExceptionMessage, actionToCall = ui::requestWinningNumbers) } diff --git a/src/main/kotlin/lotto/domain/validation/ValidateWinningNumbers.kt b/src/main/kotlin/lotto/domain/validation/ValidateWinningNumbers.kt new file mode 100644 index 000000000..54d93a2ac --- /dev/null +++ b/src/main/kotlin/lotto/domain/validation/ValidateWinningNumbers.kt @@ -0,0 +1,8 @@ +package lotto.domain.validation + +import lotto.domain.model.Lotto + +fun validateWinningNumbers(winningNumbers: List) { + // 로또 생성으로 당첨 번호 검증 + Lotto(winningNumbers) +} diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt index a3af7fa03..6668fe3fe 100644 --- a/src/main/kotlin/lotto/ui/Ui.kt +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -4,6 +4,7 @@ import camp.nextstep.edu.missionutils.Console import lotto.domain.exception.ExceptionMessages import lotto.domain.model.Lotto import lotto.domain.validation.validateBudget +import lotto.domain.validation.validateWinningNumbers class Ui { fun requestBudget(): Result = runCatching { @@ -30,6 +31,12 @@ class Ui { println(stringBuilder.toString()) } + fun requestWinningNumbers(): Result> = runCatching { + displayEnterWinningNumbers() + val userInput = readIntListSplitByComma().also { validateWinningNumbers(it) } + return@runCatching userInput + } + private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) private fun readInt(): Int = runCatching { @@ -39,10 +46,21 @@ class Ui { throw it } + private fun displayEnterWinningNumbers(): Unit = println(ENTER_WINNING_NUMBERS) + + private fun readIntListSplitByComma(): List = runCatching { + Console.readLine().split(WINNING_NUMBER_DELIMITER).map(String::toInt) + }.getOrElse { + if (it is NumberFormatException) throw NumberFormatException(ExceptionMessages.INVALID_NUMBER_FORMAT) + throw it + } + companion object { private const val ENTER_BUDGET_MESSAGE = "구입금액을 입력해 주세요." private const val EXCEPTION_MESSAGE_HEADER = "[ERROR]" private const val AMOUNT_MESSAGE = "개를 구매했습니다." private const val NEW_LINE_FEED = '\n' + private const val ENTER_WINNING_NUMBERS = "당첨 번호를 입력해 주세요." + private const val WINNING_NUMBER_DELIMITER = ',' } } diff --git a/src/test/kotlin/lotto/domain/validation/ValidateWinningNumbersKtTest.kt b/src/test/kotlin/lotto/domain/validation/ValidateWinningNumbersKtTest.kt new file mode 100644 index 000000000..355cbebdd --- /dev/null +++ b/src/test/kotlin/lotto/domain/validation/ValidateWinningNumbersKtTest.kt @@ -0,0 +1,50 @@ +package lotto.domain.validation + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ValidateWinningNumbersKtTest { + @Test + fun `당첨 번호가 6개이며 모두 유효한 범위 내에 있으면 아무 일도 일어나지 않음`() { + // act, assert + assertDoesNotThrow { validateWinningNumbers(listOf(1, 2, 3, 4, 5, 6)) } + } + + @Test + fun `당첨 번호에 중복이 있을 경우 예외 발생`() { + // act, assert + assertThrows { validateWinningNumbers(listOf(1, 2, 3, 4, 5, 5)) } + } + + @ParameterizedTest + @MethodSource(value = ["getInvalidLengthWinningNumbers"]) + fun `당첨 번호가 6개가 아닌 경우 예외 발생`(winningNumbers: List) { + // act, assert + assertThrows { validateWinningNumbers(winningNumbers) } + } + + @ParameterizedTest + @MethodSource(value = ["getWinningNumbersIncludingOutOfRangeValue"]) + fun `당첨 번호 중 유효한 범위 외의 숫자가 존재할 경우 예외 발생`(winningNumbers: List) { + // act, assert + assertThrows { validateWinningNumbers(winningNumbers) } + } + + companion object { + @JvmStatic + fun getInvalidLengthWinningNumbers(): Stream> = Stream.of( + listOf(1, 2, 3, 4, 5), + listOf(1, 2, 3, 4, 5, 6, 7) + ) + + @JvmStatic + fun getWinningNumbersIncludingOutOfRangeValue(): Stream> = Stream.of( + listOf(1, 2, 3, 4, 5, 0), + listOf(1, 2, 3, 4, 5, 46) + ) + } +} From a1b96716515581c0b604c824bc9cb9a89e0db5ae Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 20:26:42 +0900 Subject: [PATCH 12/17] =?UTF-8?q?docs:=20=EB=B3=B4=EB=84=88=EC=8A=A4=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=EC=97=90=20=EA=B4=80=ED=95=9C=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dbb688795..698b1e21c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - 당첨 번호 중 1부터 45 사이의 숫자가 아닌 것이 있으면 예외 발생 4. **보너스 번호 입력** - 당첨 번호와 마찬가지로 1부터 45 사이의 숫자가 아닌 것이 있으면 예외 발생 + - 보너스 번호와 당첨 번호가 중복이 될 경우 예외 발생 5. **결과 출력** - 생성된 로또와 당첨 번호로 당첨 결과 출력 6. **수익률 출력** From 428fedcf5253dcdda5742ac9bdea5001b6945d8c Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 20:45:25 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20=EB=B3=B4=EB=84=88=EC=8A=A4=20?= =?UTF-8?q?=EB=8B=B9=EC=B2=A8=20=EB=B2=88=ED=98=B8=EB=A5=BC=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 5 +++ .../domain/exception/ExceptionMessages.kt | 1 + .../validation/ValidateBonusWinnningNumber.kt | 11 ++++++ src/main/kotlin/lotto/ui/Ui.kt | 10 ++++++ .../ValidateBonusWinnningNumberKtTest.kt | 36 +++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 src/main/kotlin/lotto/domain/validation/ValidateBonusWinnningNumber.kt create mode 100644 src/test/kotlin/lotto/domain/validation/ValidateBonusWinnningNumberKtTest.kt diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index af15e6550..baf9e4946 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -20,4 +20,9 @@ fun main() { val winningNumbers = keepCallingForSuccessResult(onFailure = ui::displayExceptionMessage, actionToCall = ui::requestWinningNumbers) + + val bonusWinningNumber = keepCallingForSuccessResult( + onFailure = ui::displayExceptionMessage, + actionToCall = { ui.requestBonusWinningNumber(winningNumbers) } + ) } diff --git a/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt index f35c5b037..4574c9b9c 100644 --- a/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt +++ b/src/main/kotlin/lotto/domain/exception/ExceptionMessages.kt @@ -14,4 +14,5 @@ object ExceptionMessages { val NUMBER_OUT_OF_VALID_LOTTO_RANGE_EXISTS = "로또 번호는 ${VALID_LOTTO_NUMBER_RANGE.first}와 ${VALID_LOTTO_NUMBER_RANGE.last} 사이의 값이어야 합니다." const val DUPLICATE_NUMBER_EXISTS = "로또 번호에 중복이 없어야 합니다." + const val DUPLICATE_BONUS_WINNING_NUMBER = "보너스 당첨 번호는 당첨 번호와 중복이 될 수 없습니다." } diff --git a/src/main/kotlin/lotto/domain/validation/ValidateBonusWinnningNumber.kt b/src/main/kotlin/lotto/domain/validation/ValidateBonusWinnningNumber.kt new file mode 100644 index 000000000..d79192f6c --- /dev/null +++ b/src/main/kotlin/lotto/domain/validation/ValidateBonusWinnningNumber.kt @@ -0,0 +1,11 @@ +package lotto.domain.validation + +import lotto.domain.exception.ExceptionMessages +import lotto.domain.model.Lotto + +fun validateBonusWinningNumber(bonusWinningNumber: Int, winningNumbers: List) { + require(bonusWinningNumber in Lotto.VALID_LOTTO_NUMBER_RANGE) { + ExceptionMessages.NUMBER_OUT_OF_VALID_LOTTO_RANGE_EXISTS + } + require(bonusWinningNumber !in winningNumbers) { ExceptionMessages.DUPLICATE_BONUS_WINNING_NUMBER } +} diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt index 6668fe3fe..94c4ce821 100644 --- a/src/main/kotlin/lotto/ui/Ui.kt +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -3,6 +3,7 @@ package lotto.ui import camp.nextstep.edu.missionutils.Console import lotto.domain.exception.ExceptionMessages import lotto.domain.model.Lotto +import lotto.domain.validation.validateBonusWinningNumber import lotto.domain.validation.validateBudget import lotto.domain.validation.validateWinningNumbers @@ -37,6 +38,12 @@ class Ui { return@runCatching userInput } + fun requestBonusWinningNumber(winningNumbers: List): Result = runCatching { + displayEnterBonusWinningNumber() + val userInput = readInt().also { validateBonusWinningNumber(it, winningNumbers) } + return@runCatching userInput + } + private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) private fun readInt(): Int = runCatching { @@ -55,6 +62,8 @@ class Ui { throw it } + private fun displayEnterBonusWinningNumber(): Unit = println("$NEW_LINE_FEED$ENTER_BONUS_WINNING_NUMBER") + companion object { private const val ENTER_BUDGET_MESSAGE = "구입금액을 입력해 주세요." private const val EXCEPTION_MESSAGE_HEADER = "[ERROR]" @@ -62,5 +71,6 @@ class Ui { private const val NEW_LINE_FEED = '\n' private const val ENTER_WINNING_NUMBERS = "당첨 번호를 입력해 주세요." private const val WINNING_NUMBER_DELIMITER = ',' + private const val ENTER_BONUS_WINNING_NUMBER = "보너스 번호를 입력해 주세요." } } diff --git a/src/test/kotlin/lotto/domain/validation/ValidateBonusWinnningNumberKtTest.kt b/src/test/kotlin/lotto/domain/validation/ValidateBonusWinnningNumberKtTest.kt new file mode 100644 index 000000000..1acd6de2e --- /dev/null +++ b/src/test/kotlin/lotto/domain/validation/ValidateBonusWinnningNumberKtTest.kt @@ -0,0 +1,36 @@ +package lotto.domain.validation + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ValidateBonusWinnningNumberKtTest { + @ParameterizedTest + @ValueSource(ints = [1, 45]) + fun `보너스 당첨 번호가 올바른 범위 내에 있고 당첨 번호화 중복이 되지 않을 경우 아무 일도 일어나지 않음`(bonusWinningNumber: Int) { + val winningNumbers = listOf(2, 3, 4, 5, 6, 7) + + // act, assert + assertDoesNotThrow { validateBonusWinningNumber(bonusWinningNumber, winningNumbers) } + } + + @ParameterizedTest + @ValueSource(ints = [0, 46]) + fun `보너스 당첨 번호가 올바른 범위 내에 있지 않을 경우 예외 발생`(bonusWinningNumber: Int) { + val winningNumbers = listOf(2, 3, 4, 5, 6, 7) + + // act, assert + assertThrows { validateBonusWinningNumber(bonusWinningNumber, winningNumbers) } + } + + @Test + fun `보너스 당첨 번호와 당첨 번호가 중복이 될 경우 예외 발생`() { + // arrange + val winningNumbers = listOf(1, 2, 3, 4, 5, 6) + + // act, assert + assertThrows { validateBonusWinningNumber(1, winningNumbers) } + } +} From cdfffd2176b2d458c1b91f0568f1b16eb733db17 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 22:32:46 +0900 Subject: [PATCH 14/17] =?UTF-8?q?refactor:=20Ui=EB=A5=BC=20OutputView?= =?UTF-8?q?=EC=99=80=20InputView=EB=A1=9C=20=EB=82=98=EB=88=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/ui/InputView.kt | 7 ++ src/main/kotlin/lotto/ui/OutputView.kt | 9 +++ src/main/kotlin/lotto/ui/Ui.kt | 76 +------------------ .../lotto/ui/console/ConsoleInputView.kt | 56 ++++++++++++++ .../lotto/ui/console/ConsoleOutputView.kt | 31 ++++++++ 5 files changed, 106 insertions(+), 73 deletions(-) create mode 100644 src/main/kotlin/lotto/ui/InputView.kt create mode 100644 src/main/kotlin/lotto/ui/OutputView.kt create mode 100644 src/main/kotlin/lotto/ui/console/ConsoleInputView.kt create mode 100644 src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt diff --git a/src/main/kotlin/lotto/ui/InputView.kt b/src/main/kotlin/lotto/ui/InputView.kt new file mode 100644 index 000000000..594398cbb --- /dev/null +++ b/src/main/kotlin/lotto/ui/InputView.kt @@ -0,0 +1,7 @@ +package lotto.ui + +interface InputView { + fun requestBudget(): Result + fun requestWinningNumbers(): Result> + fun requestBonusWinningNumber(winningNumbers: List): Result +} diff --git a/src/main/kotlin/lotto/ui/OutputView.kt b/src/main/kotlin/lotto/ui/OutputView.kt new file mode 100644 index 000000000..298b8289c --- /dev/null +++ b/src/main/kotlin/lotto/ui/OutputView.kt @@ -0,0 +1,9 @@ +package lotto.ui + +import lotto.domain.model.Lotto + +interface OutputView { + fun displayExceptionMessage(exception: Throwable) + fun displayAmount(amount: Int) + fun displayLottoes(lottoes: List) +} diff --git a/src/main/kotlin/lotto/ui/Ui.kt b/src/main/kotlin/lotto/ui/Ui.kt index 94c4ce821..026b6c80c 100644 --- a/src/main/kotlin/lotto/ui/Ui.kt +++ b/src/main/kotlin/lotto/ui/Ui.kt @@ -1,76 +1,6 @@ package lotto.ui -import camp.nextstep.edu.missionutils.Console -import lotto.domain.exception.ExceptionMessages -import lotto.domain.model.Lotto -import lotto.domain.validation.validateBonusWinningNumber -import lotto.domain.validation.validateBudget -import lotto.domain.validation.validateWinningNumbers +import lotto.ui.console.ConsoleInputView +import lotto.ui.console.ConsoleOutputView -class Ui { - fun requestBudget(): Result = runCatching { - displayEnterBudgetMessage() - val userInput = readInt().also { validateBudget(it) } - return@runCatching userInput - } - - fun displayExceptionMessage(exception: Throwable) { - val message = exception.message ?: ExceptionMessages.DEFAULT_EXCEPTION_MESSAGE - println("$EXCEPTION_MESSAGE_HEADER $message") - } - - fun displayAmount(amount: Int): Unit = println("${NEW_LINE_FEED}${amount}${AMOUNT_MESSAGE}") - - fun displayLottoes(lottoes: List) { - val stringBuilder = StringBuilder() - - for (lotto in lottoes) { - stringBuilder.append(lotto.toString()) - stringBuilder.append(NEW_LINE_FEED) - } - - println(stringBuilder.toString()) - } - - fun requestWinningNumbers(): Result> = runCatching { - displayEnterWinningNumbers() - val userInput = readIntListSplitByComma().also { validateWinningNumbers(it) } - return@runCatching userInput - } - - fun requestBonusWinningNumber(winningNumbers: List): Result = runCatching { - displayEnterBonusWinningNumber() - val userInput = readInt().also { validateBonusWinningNumber(it, winningNumbers) } - return@runCatching userInput - } - - private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) - - private fun readInt(): Int = runCatching { - Console.readLine().toInt() - }.getOrElse { - if (it is NumberFormatException) throw NumberFormatException(ExceptionMessages.INVALID_NUMBER_FORMAT) - throw it - } - - private fun displayEnterWinningNumbers(): Unit = println(ENTER_WINNING_NUMBERS) - - private fun readIntListSplitByComma(): List = runCatching { - Console.readLine().split(WINNING_NUMBER_DELIMITER).map(String::toInt) - }.getOrElse { - if (it is NumberFormatException) throw NumberFormatException(ExceptionMessages.INVALID_NUMBER_FORMAT) - throw it - } - - private fun displayEnterBonusWinningNumber(): Unit = println("$NEW_LINE_FEED$ENTER_BONUS_WINNING_NUMBER") - - companion object { - private const val ENTER_BUDGET_MESSAGE = "구입금액을 입력해 주세요." - private const val EXCEPTION_MESSAGE_HEADER = "[ERROR]" - private const val AMOUNT_MESSAGE = "개를 구매했습니다." - private const val NEW_LINE_FEED = '\n' - private const val ENTER_WINNING_NUMBERS = "당첨 번호를 입력해 주세요." - private const val WINNING_NUMBER_DELIMITER = ',' - private const val ENTER_BONUS_WINNING_NUMBER = "보너스 번호를 입력해 주세요." - } -} +class Ui : OutputView by ConsoleOutputView(), InputView by ConsoleInputView() diff --git a/src/main/kotlin/lotto/ui/console/ConsoleInputView.kt b/src/main/kotlin/lotto/ui/console/ConsoleInputView.kt new file mode 100644 index 000000000..919d188ff --- /dev/null +++ b/src/main/kotlin/lotto/ui/console/ConsoleInputView.kt @@ -0,0 +1,56 @@ +package lotto.ui.console + +import camp.nextstep.edu.missionutils.Console +import lotto.domain.exception.ExceptionMessages +import lotto.domain.validation.validateBonusWinningNumber +import lotto.domain.validation.validateBudget +import lotto.domain.validation.validateWinningNumbers +import lotto.ui.InputView + +class ConsoleInputView : InputView { + override fun requestBudget(): Result = runCatching { + displayEnterBudgetMessage() + val userInput = readInt().also { validateBudget(it) } + return@runCatching userInput + } + + override fun requestWinningNumbers(): Result> = runCatching { + displayEnterWinningNumbers() + val userInput = readIntListSplitByComma().also { validateWinningNumbers(it) } + return@runCatching userInput + } + + override fun requestBonusWinningNumber(winningNumbers: List): Result = runCatching { + displayEnterBonusWinningNumber() + val userInput = readInt().also { validateBonusWinningNumber(it, winningNumbers) } + return@runCatching userInput + } + + private fun displayEnterBudgetMessage(): Unit = println(ENTER_BUDGET_MESSAGE) + + private fun readInt(): Int = runCatching { + Console.readLine().toInt() + }.getOrElse { + if (it is NumberFormatException) throw NumberFormatException(ExceptionMessages.INVALID_NUMBER_FORMAT) + throw it + } + + private fun displayEnterWinningNumbers(): Unit = println(ENTER_WINNING_NUMBERS) + + private fun readIntListSplitByComma(): List = runCatching { + Console.readLine().split(WINNING_NUMBER_DELIMITER).map(String::toInt) + }.getOrElse { + if (it is NumberFormatException) throw NumberFormatException(ExceptionMessages.INVALID_NUMBER_FORMAT) + throw it + } + + private fun displayEnterBonusWinningNumber(): Unit = println("$NEW_LINE_FEED$ENTER_BONUS_WINNING_NUMBER") + + companion object { + private const val ENTER_BUDGET_MESSAGE = "구입금액을 입력해 주세요." + private const val ENTER_WINNING_NUMBERS = "당첨 번호를 입력해 주세요." + private const val WINNING_NUMBER_DELIMITER = ',' + private const val ENTER_BONUS_WINNING_NUMBER = "보너스 번호를 입력해 주세요." + private const val NEW_LINE_FEED = '\n' + } +} diff --git a/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt b/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt new file mode 100644 index 000000000..5e1b8aca9 --- /dev/null +++ b/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt @@ -0,0 +1,31 @@ +package lotto.ui.console + +import lotto.domain.exception.ExceptionMessages +import lotto.domain.model.Lotto +import lotto.ui.OutputView + +class ConsoleOutputView : OutputView { + override fun displayExceptionMessage(exception: Throwable) { + val message = exception.message ?: ExceptionMessages.DEFAULT_EXCEPTION_MESSAGE + println("$EXCEPTION_MESSAGE_HEADER $message") + } + + override fun displayAmount(amount: Int): Unit = println("$NEW_LINE_FEED${amount}$AMOUNT_MESSAGE") + + override fun displayLottoes(lottoes: List) { + val stringBuilder = StringBuilder() + + for (lotto in lottoes) { + stringBuilder.append(lotto.toString()) + stringBuilder.append(NEW_LINE_FEED) + } + + println(stringBuilder.toString()) + } + + companion object { + private const val EXCEPTION_MESSAGE_HEADER = "[ERROR]" + private const val AMOUNT_MESSAGE = "개를 구매했습니다." + private const val NEW_LINE_FEED = '\n' + } +} From 5f535241e1ad3b46a9f6b1b5875984141b13c181 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 22:43:21 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=9E=A8=EC=9D=98=20=ED=9D=90=EB=A6=84=EC=9D=84=20=EC=A0=84?= =?UTF-8?q?=EB=B0=98=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=A0=20LottoApp=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/Application.kt | 19 ++--------------- src/main/kotlin/lotto/LottoApp.kt | 31 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/lotto/LottoApp.kt diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index baf9e4946..50a9dfaaa 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,28 +1,13 @@ package lotto import lotto.data.random.pickLottoNumbers -import lotto.domain.model.Lotto import lotto.domain.model.factory.LottoFactory import lotto.ui.Ui -import lotto.util.keepCallingForSuccessResult fun main() { val ui = Ui() - - val budget = keepCallingForSuccessResult(onFailure = ui::displayExceptionMessage, actionToCall = ui::requestBudget) - - val amount = budget / Lotto.LOTTO_PRICE - ui.displayAmount(amount) - val lottoFactory = LottoFactory(lottoNumberGenerator = ::pickLottoNumbers) - val lottoes = lottoFactory.createLottoes(amount) - ui.displayLottoes(lottoes) - - val winningNumbers = - keepCallingForSuccessResult(onFailure = ui::displayExceptionMessage, actionToCall = ui::requestWinningNumbers) + val app = LottoApp(inputView = ui, outputView = ui, lottoFactory = lottoFactory) - val bonusWinningNumber = keepCallingForSuccessResult( - onFailure = ui::displayExceptionMessage, - actionToCall = { ui.requestBonusWinningNumber(winningNumbers) } - ) + app.run() } diff --git a/src/main/kotlin/lotto/LottoApp.kt b/src/main/kotlin/lotto/LottoApp.kt new file mode 100644 index 000000000..049beb67b --- /dev/null +++ b/src/main/kotlin/lotto/LottoApp.kt @@ -0,0 +1,31 @@ +package lotto + +import lotto.domain.model.Lotto +import lotto.domain.model.factory.LottoFactory +import lotto.ui.InputView +import lotto.ui.OutputView +import lotto.util.keepCallingForSuccessResult + +class LottoApp( + private val outputView: OutputView, + private val inputView: InputView, + private val lottoFactory: LottoFactory +) { + fun run() { + val budget = keepCallingWithDefaultOnFailure(inputView::requestBudget) + + val amount = budget / Lotto.LOTTO_PRICE + outputView.displayAmount(amount) + + val lottoes = lottoFactory.createLottoes(amount) + outputView.displayLottoes(lottoes) + + val winningNumbers = keepCallingWithDefaultOnFailure(inputView::requestWinningNumbers) + val bonusWinningNumber = keepCallingWithDefaultOnFailure { inputView.requestBonusWinningNumber(winningNumbers) } + } + + private fun keepCallingWithDefaultOnFailure(actionToCall: () -> Result): R = keepCallingForSuccessResult( + onFailure = outputView::displayExceptionMessage, + actionToCall = actionToCall, + ) +} From a425938805f4e39722505b325d89755df5759d67 Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 23:48:36 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EB=A5=BC=20=EA=B3=84=EC=82=B0=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/LottoApp.kt | 10 ++++ src/main/kotlin/lotto/domain/model/Lotto.kt | 8 +++ .../lotto/domain/model/LottoWinPlace.kt | 50 +++++++++++++++++++ src/main/kotlin/lotto/ui/OutputView.kt | 2 + .../lotto/ui/console/ConsoleOutputView.kt | 31 ++++++++++++ .../kotlin/lotto/domain/model/LottoTest.kt | 33 ++++++++++++ 6 files changed, 134 insertions(+) create mode 100644 src/main/kotlin/lotto/domain/model/LottoWinPlace.kt diff --git a/src/main/kotlin/lotto/LottoApp.kt b/src/main/kotlin/lotto/LottoApp.kt index 049beb67b..7c5074375 100644 --- a/src/main/kotlin/lotto/LottoApp.kt +++ b/src/main/kotlin/lotto/LottoApp.kt @@ -1,6 +1,7 @@ package lotto import lotto.domain.model.Lotto +import lotto.domain.model.LottoWinPlace import lotto.domain.model.factory.LottoFactory import lotto.ui.InputView import lotto.ui.OutputView @@ -22,8 +23,17 @@ class LottoApp( val winningNumbers = keepCallingWithDefaultOnFailure(inputView::requestWinningNumbers) val bonusWinningNumber = keepCallingWithDefaultOnFailure { inputView.requestBonusWinningNumber(winningNumbers) } + + val lottoWinPlaces = lottoes.calculateAllLottoWinPlaces(winningNumbers, bonusWinningNumber) + outputView.displayLottoResults(lottoWinPlaces) } + private fun List.calculateAllLottoWinPlaces( + winningNumbers: List, + bonusWinningNumber: Int + ): Map = + mapNotNull { it.calculateWinPlace(winningNumbers, bonusWinningNumber) }.groupingBy { it }.eachCount() + private fun keepCallingWithDefaultOnFailure(actionToCall: () -> Result): R = keepCallingForSuccessResult( onFailure = outputView::displayExceptionMessage, actionToCall = actionToCall, diff --git a/src/main/kotlin/lotto/domain/model/Lotto.kt b/src/main/kotlin/lotto/domain/model/Lotto.kt index f4521e451..73b394a6f 100644 --- a/src/main/kotlin/lotto/domain/model/Lotto.kt +++ b/src/main/kotlin/lotto/domain/model/Lotto.kt @@ -11,6 +11,14 @@ class Lotto(private val numbers: List) { } } + fun calculateWinPlace(winningNumbers: List, bonusWinningNumber: Int): LottoWinPlace? { + val matchedNumbers = getMatchedNumbers(winningNumbers) + val isBonusWinningNumberMatched = bonusWinningNumber in numbers + return LottoWinPlace.findLottoWinPlace(matchedNumbers.size, isBonusWinningNumberMatched) + } + + private fun getMatchedNumbers(winningNumbers: List) = numbers.toSet().intersect(winningNumbers.toSet()) + override fun toString(): String = numbers.sorted().toString() companion object { diff --git a/src/main/kotlin/lotto/domain/model/LottoWinPlace.kt b/src/main/kotlin/lotto/domain/model/LottoWinPlace.kt new file mode 100644 index 000000000..119b8a901 --- /dev/null +++ b/src/main/kotlin/lotto/domain/model/LottoWinPlace.kt @@ -0,0 +1,50 @@ +package lotto.domain.model + +enum class LottoWinPlace( + val prize: Int, + val requiredMatchedNumberLength: Int, + val shouldBonusWinningBeNumberMatched: Boolean, +) { + FIRST_PLACE( + prize = 2_000_000_000, + requiredMatchedNumberLength = 6, + shouldBonusWinningBeNumberMatched = false + ), + SECOND_PLACE( + prize = 30_000_000, + requiredMatchedNumberLength = 5, + shouldBonusWinningBeNumberMatched = true + ), + THIRD_PLACE( + prize = 1_500_000, + requiredMatchedNumberLength = 5, + shouldBonusWinningBeNumberMatched = false + ), + FOURTH_PLACE( + prize = 50_000, + requiredMatchedNumberLength = 4, + shouldBonusWinningBeNumberMatched = false + ), + FIFTH_PLACE( + prize = 5_000, + requiredMatchedNumberLength = 3, + shouldBonusWinningBeNumberMatched = false + ); + + companion object { + fun findLottoWinPlace( + matchedNumberLength: Int, + isBonusWinningNumberMatched: Boolean, + ): LottoWinPlace? { + var candidates = entries.filter { it.requiredMatchedNumberLength == matchedNumberLength } + + if (candidates.hasMoreThanOneElement()) { + candidates = candidates.filter { it.shouldBonusWinningBeNumberMatched == isBonusWinningNumberMatched } + } + + return candidates.firstOrNull() + } + + private fun List.hasMoreThanOneElement(): Boolean = size > 1 + } +} diff --git a/src/main/kotlin/lotto/ui/OutputView.kt b/src/main/kotlin/lotto/ui/OutputView.kt index 298b8289c..55778b04d 100644 --- a/src/main/kotlin/lotto/ui/OutputView.kt +++ b/src/main/kotlin/lotto/ui/OutputView.kt @@ -1,9 +1,11 @@ package lotto.ui import lotto.domain.model.Lotto +import lotto.domain.model.LottoWinPlace interface OutputView { fun displayExceptionMessage(exception: Throwable) fun displayAmount(amount: Int) fun displayLottoes(lottoes: List) + fun displayLottoResults(lottoWinPlaces: Map) } diff --git a/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt b/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt index 5e1b8aca9..17640fbc1 100644 --- a/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt +++ b/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt @@ -2,6 +2,7 @@ package lotto.ui.console import lotto.domain.exception.ExceptionMessages import lotto.domain.model.Lotto +import lotto.domain.model.LottoWinPlace import lotto.ui.OutputView class ConsoleOutputView : OutputView { @@ -23,9 +24,39 @@ class ConsoleOutputView : OutputView { println(stringBuilder.toString()) } + override fun displayLottoResults(lottoWinPlaces: Map) { + val stringBuilder = StringBuilder() + val availableWinPlaces = LottoWinPlace.entries.reversed() + + stringBuilder.appendLine() + stringBuilder.appendLine(RESULT_HEADER) + stringBuilder.appendLine(SECTION_SEPARATOR) + + for (availableWinPlace in availableWinPlaces) { + val winPlaceCount = lottoWinPlaces[availableWinPlace] ?: 0 + stringBuilder.appendLine(availableWinPlace.convertToString(winPlaceCount)) + } + + println(stringBuilder.toString()) + } + + private fun LottoWinPlace.convertToString(count: Int): String = + "${requiredMatchedNumberLength}개 일치${bonusWinningNumberMatched()} (${PRIZE_FORMAT.format(prize)}$KRW) - ${count}개" + + private fun LottoWinPlace.bonusWinningNumberMatched(): String { + if (shouldBonusWinningBeNumberMatched) return BONUS_WINNING_NUMBER_MATCH + return EMPTY_STRING + } + companion object { private const val EXCEPTION_MESSAGE_HEADER = "[ERROR]" private const val AMOUNT_MESSAGE = "개를 구매했습니다." private const val NEW_LINE_FEED = '\n' + private const val PRIZE_FORMAT = "%,d" + private const val KRW = "원" + private const val RESULT_HEADER = "당첨 통계" + private const val SECTION_SEPARATOR = "---" + private const val BONUS_WINNING_NUMBER_MATCH = ", 보너스 볼 일치" + private const val EMPTY_STRING = "" } } diff --git a/src/test/kotlin/lotto/domain/model/LottoTest.kt b/src/test/kotlin/lotto/domain/model/LottoTest.kt index 8288f094c..5e2a96a4d 100644 --- a/src/test/kotlin/lotto/domain/model/LottoTest.kt +++ b/src/test/kotlin/lotto/domain/model/LottoTest.kt @@ -4,6 +4,10 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream class LottoTest { @@ -57,4 +61,33 @@ class LottoTest { val expectedResult = "[1, 2, 3, 4, 5, 6]" assertThat(result).isEqualTo(expectedResult) } + + @ParameterizedTest + @MethodSource(value = ["provideParametersForCalculateWinPlace"]) + fun `로또 번호와 당첨 번호, 보너스 번호가 주어질 경우 알맞은 등수를 반환`( + winningNumbers: List, + bonusWinningNumber: Int, + expectedResult: LottoWinPlace?, + ) { + // arrange + val lotto = Lotto(listOf(6, 5, 4, 3, 2, 1)) + + // act + val lottoWinPlace = lotto.calculateWinPlace(winningNumbers, bonusWinningNumber) + + // assert + assertThat(lottoWinPlace).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun provideParametersForCalculateWinPlace(): Stream = Stream.of( + Arguments.of(listOf(1, 2, 3, 4, 5, 6), 7, LottoWinPlace.FIRST_PLACE), + Arguments.of(listOf(1, 2, 3, 4, 5, 45), 6, LottoWinPlace.SECOND_PLACE), + Arguments.of(listOf(1, 2, 3, 4, 5, 45), 44, LottoWinPlace.THIRD_PLACE), + Arguments.of(listOf(1, 2, 3, 4, 44, 45), 7, LottoWinPlace.FOURTH_PLACE), + Arguments.of(listOf(1, 2, 3, 43, 44, 34), 7, LottoWinPlace.FIFTH_PLACE), + Arguments.of(listOf(1, 2, 42, 43, 44, 45), 7, null), + ) + } } From 4c840f20e786038b3cc22ab094aecbbaecaf02ad Mon Sep 17 00:00:00 2001 From: ErroredPasta Date: Mon, 4 Nov 2024 23:57:28 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20=EC=88=98=EC=9D=B5=EB=A5=A0=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/lotto/LottoApp.kt | 2 +- src/main/kotlin/lotto/domain/CalculateProfit.kt | 11 +++++++++++ src/main/kotlin/lotto/ui/OutputView.kt | 2 +- src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt | 7 ++++++- 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/lotto/domain/CalculateProfit.kt diff --git a/src/main/kotlin/lotto/LottoApp.kt b/src/main/kotlin/lotto/LottoApp.kt index 7c5074375..1cfb43a87 100644 --- a/src/main/kotlin/lotto/LottoApp.kt +++ b/src/main/kotlin/lotto/LottoApp.kt @@ -25,7 +25,7 @@ class LottoApp( val bonusWinningNumber = keepCallingWithDefaultOnFailure { inputView.requestBonusWinningNumber(winningNumbers) } val lottoWinPlaces = lottoes.calculateAllLottoWinPlaces(winningNumbers, bonusWinningNumber) - outputView.displayLottoResults(lottoWinPlaces) + outputView.displayLottoResults(lottoWinPlaces, budget) } private fun List.calculateAllLottoWinPlaces( diff --git a/src/main/kotlin/lotto/domain/CalculateProfit.kt b/src/main/kotlin/lotto/domain/CalculateProfit.kt new file mode 100644 index 000000000..8ebd55456 --- /dev/null +++ b/src/main/kotlin/lotto/domain/CalculateProfit.kt @@ -0,0 +1,11 @@ +package lotto.domain + +import lotto.domain.model.LottoWinPlace + +private const val PERCENT = 100 + +fun calculateProfitRate(budget: Int, lottoWinPlaces: Map): Float { + val totalPrize = lottoWinPlaces.map { (lottoWinPlace, count) -> lottoWinPlace.prize * count }.sum().toFloat() + return (totalPrize / budget) * PERCENT +} + diff --git a/src/main/kotlin/lotto/ui/OutputView.kt b/src/main/kotlin/lotto/ui/OutputView.kt index 55778b04d..2253d0530 100644 --- a/src/main/kotlin/lotto/ui/OutputView.kt +++ b/src/main/kotlin/lotto/ui/OutputView.kt @@ -7,5 +7,5 @@ interface OutputView { fun displayExceptionMessage(exception: Throwable) fun displayAmount(amount: Int) fun displayLottoes(lottoes: List) - fun displayLottoResults(lottoWinPlaces: Map) + fun displayLottoResults(lottoWinPlaces: Map, budget: Int) } diff --git a/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt b/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt index 17640fbc1..a7990cc5c 100644 --- a/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt +++ b/src/main/kotlin/lotto/ui/console/ConsoleOutputView.kt @@ -1,5 +1,6 @@ package lotto.ui.console +import lotto.domain.calculateProfitRate import lotto.domain.exception.ExceptionMessages import lotto.domain.model.Lotto import lotto.domain.model.LottoWinPlace @@ -24,7 +25,7 @@ class ConsoleOutputView : OutputView { println(stringBuilder.toString()) } - override fun displayLottoResults(lottoWinPlaces: Map) { + override fun displayLottoResults(lottoWinPlaces: Map, budget: Int) { val stringBuilder = StringBuilder() val availableWinPlaces = LottoWinPlace.entries.reversed() @@ -37,6 +38,9 @@ class ConsoleOutputView : OutputView { stringBuilder.appendLine(availableWinPlace.convertToString(winPlaceCount)) } + val profit = calculateProfitRate(budget, lottoWinPlaces) + + stringBuilder.appendLine(PROFIT_FORMAT.format(profit)) println(stringBuilder.toString()) } @@ -58,5 +62,6 @@ class ConsoleOutputView : OutputView { private const val SECTION_SEPARATOR = "---" private const val BONUS_WINNING_NUMBER_MATCH = ", 보너스 볼 일치" private const val EMPTY_STRING = "" + private const val PROFIT_FORMAT = "총 수익률은 %.1f%%입니다." } }