diff --git a/__tests__/LottoTest.js b/__tests__/domain/LottoTest.js similarity index 51% rename from __tests__/LottoTest.js rename to __tests__/domain/LottoTest.js index 97bd457659..e17d672c6e 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/domain/LottoTest.js @@ -1,4 +1,4 @@ -import Lotto from "../src/Lotto.js"; +import Lotto from "../../src/domain/Lotto"; describe("로또 클래스 테스트", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { @@ -7,12 +7,20 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); - // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); }).toThrow("[ERROR]"); }); - // 아래에 추가 테스트 작성 가능 + test("로또 번호 1~45 범위를 초과하면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR]"); + }); + test("로또 번호가 자연수가 아니면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6.5]); + }).toThrow("[ERROR]"); + }); }); diff --git a/__tests__/view/InputTest.js b/__tests__/view/InputTest.js new file mode 100644 index 0000000000..d2dd5666d2 --- /dev/null +++ b/__tests__/view/InputTest.js @@ -0,0 +1,34 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import InputView from "../../src/view/input-view"; + +const mockInput = (input) => { + MissionUtils.Console.readLineAsync = jest.fn(); + MissionUtils.Console.readLineAsync.mockResolvedValue(input); +}; + +describe("입력값 예외처리 테스트", () => { + test("구입 금액이 1000으로 나누어 떨어지지 않을 경우 예외 처리", async () => { + //given + const input = 14500; + mockInput(input); + + //when + const inputValue = new InputView(); + + //then + await expect(inputValue.readPurchaseAmount()).rejects.toThrow("[ERROR]"); + }); + + test.each([ + 0, -1000 + ])("구입 금액이 0 이하 일 경우 예외 처리", async (inputs) => { + //given + mockInput(inputs); + + //when + const inputValue = new InputView(); + + //then + await expect(inputValue.readPurchaseAmount()).rejects.toThrow("[ERROR]"); + }); +}); \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000000..deb7eb780b --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,160 @@ +## 기능 요구 사항 + + 로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다. + +``` +- 로또 번호의 숫자 범위는 1~45까지이다. +- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다. +- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다. +- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다. + - 1등: 6개 번호 일치 / 2,000,000,000원 + - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 + - 3등: 5개 번호 일치 / 1,500,000원 + - 4등: 4개 번호 일치 / 50,000원 + - 5등: 3개 번호 일치 / 5,000원 +``` + +- 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다. +- 로또 1장의 가격은 1,000원이다. +- 당첨 번호와 보너스 번호를 입력받는다. +- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다. +- 사용자가 잘못된 값을 입력할 경우 `throw`문을 사용해 예외를 발생시킨다. 그런 다음, "[ERROR]"로 시작하는 에러 메시지를 출력하고 해당 부분부터 입력을 다시 받는다. + ``` + 예시) [ERROR] 숫자가 잘못된 형식입니다. + ``` + +### 입출력 요구 사항 + +#### 입력 + +- 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다. + +``` +14000 +``` + +- 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다. + +``` +1,2,3,4,5,6 +``` + +- 보너스 번호를 입력 받는다. + +``` +7 +``` + +#### 출력 + +- 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다. + +``` +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] +``` + +- 당첨 내역을 출력한다. + +``` +3개 일치 (5,000원) - 1개 +4개 일치 (50,000원) - 0개 +5개 일치 (1,500,000원) - 0개 +5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 +6개 일치 (2,000,000,000원) - 0개 +``` + +- 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%) + +``` +총 수익률은 62.5%입니다. +``` + +- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다. + +``` +[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다. +``` + +# +### 구현 완료 여부 + +#### 입력 기능 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
완료 상태테스트
로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
로또 1장의 가격은 1,000원이다. 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.
로또 구입 금액이 0 이하일 경우 예외 처리한다.
로또 번호가 자연수가 아닐 경우 예외 처리한다.
당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.
보너스 번호를 입력받는다.
로또 번호는 1-45사이 숫자이며 아닐 경우 예외 처리한다.
로또 번호가 중복되는 숫자일 경우 예외 처리한다.
+ +
+ + +#### 출력 기능 + + + + + + + + + + + + + + + + + + + + + + + + + + +
완료 상태테스트
발행한 로또 수량 및 번호를 출력한다.
로또 번호는 오름차순으로 정렬하여 보여준다.
당첨 내역을 출력한다.
수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다
diff --git a/src/App.js b/src/App.js index c38b30d5b2..76de5dfee7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,15 @@ +import LottoController from "./controller/lotto-controller.js"; + class App { - async play() {} + #lottoController; + + constructor() { + this.#lottoController = new LottoController(); + } + + async play() { + await this.#lottoController.play(); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js deleted file mode 100644 index cb0b1527e9..0000000000 --- a/src/Lotto.js +++ /dev/null @@ -1,18 +0,0 @@ -class Lotto { - #numbers; - - constructor(numbers) { - this.#validate(numbers); - this.#numbers = numbers; - } - - #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); - } - } - - // TODO: 추가 기능 구현 -} - -export default Lotto; diff --git a/src/constants/constants.js b/src/constants/constants.js new file mode 100644 index 0000000000..902c30c7aa --- /dev/null +++ b/src/constants/constants.js @@ -0,0 +1,26 @@ +export const INPUT_MESSAGE = { + inputPurchaseAmount : "구입 금액을 입력해 주세요.\n", + inputWinningNumbers : "\n당첨 번호를 입력해 주세요.\n", + inputBonusNumber : "\n보너스 번호를 입력해 주세요.\n", +}; + +export const OUTPUT_MESSAGE = { + printPurchaseNumber : (count) => `\n${count}개 구매했습니다.`, + printWinningStatistics : "\n당첨 통계\n---", + printFifth : (count) => `3개 일치 (5,000원) - ${count}개`, + printFourth : (count) => `4개 일치 (50,000원) - ${count}개`, + printThird : (count) => `5개 일치 (1,500,000원) - ${count}개`, + printSecond : (count) => `5개 일치, 보너스 볼 일치 (30,000,000원) - ${count}개`, + printFirst : (count) => `6개 일치 (2,000,000,000원) - ${count}개`, + printRateReturn : (rate) => `총 수익률은 ${rate}%입니다.`, +}; + +export const ERROR_MESSAGE = { + purchaseError : "[ERROR] 구입 금액은 1000으로 나누어 떨어지는 수 입니다.", + purchaseRangeError : "[ERROR] 구입 금액 범위는 양수인 정수입니다.", + lottoLengthError : "[ERROR] 로또 번호는 6개여야 합니다.", + lottoDuplicatedError : "[ERROR] 로또 번호는 중복될 수 없습니다.", + lottoRangeError : "[ERROR] 로또 번호의 범위는 1~45 폐구간 입니다.", + lottoTypeError : "[ERROR] 로또 번호는 자연수입니다.", + bonusLengthError : "[ERROR] 보너스 번호는 1개여야 합니다.", +}; \ No newline at end of file diff --git a/src/controller/lotto-controller.js b/src/controller/lotto-controller.js new file mode 100644 index 0000000000..94799f1430 --- /dev/null +++ b/src/controller/lotto-controller.js @@ -0,0 +1,26 @@ +import MyLotto from "../domain/MyLotto.js"; +import InputView from "../view/input-view.js"; +import OutputView from "../view/output-view.js"; + +class LottoController { + #inputView; + #outputView; + + constructor() { + this.#inputView = new InputView(); + this.#outputView = new OutputView(); + } + + async play() { + let perchaseNumber = await this.#inputView.readPurchaseAmount(); + let myLotto = new MyLotto(perchaseNumber); + + this.#outputView.printMyLotto(perchaseNumber, myLotto); + + let winningLotto = await this.#inputView.readWinningLotto(); + + this.#outputView.printResult(myLotto, winningLotto); + } +} + +export default LottoController; \ No newline at end of file diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js new file mode 100644 index 0000000000..97ec8c8f8d --- /dev/null +++ b/src/domain/Lotto.js @@ -0,0 +1,23 @@ +import LottoValidator from "../validator/lotto-validator.js"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + LottoValidator.lottoLengthValidation(numbers); + LottoValidator.lottoDuplicatedValidation(numbers); + LottoValidator.lottoRangeValidation(numbers); + LottoValidator.lottoTypeValidation(numbers); + } + + getNumbers() { + return this.#numbers; + } +} + +export default Lotto; diff --git a/src/domain/LottoResult.js b/src/domain/LottoResult.js new file mode 100644 index 0000000000..e1e67b61ea --- /dev/null +++ b/src/domain/LottoResult.js @@ -0,0 +1,52 @@ +class LottoResult{ + #myLotto; + #winningLotto; + #matching; + #matchReward; + + constructor(myLotto, winningLotto) { + this.#myLotto = myLotto; + this.#winningLotto = winningLotto; + this.#matching = [0, 0, 0, 0, 0]; + this.#matchReward = [5000, 50000, 1500000, 30000000, 2000000000]; + this.#compareLotto(); + } + + #updateMatch(match, isHave) { + switch(match){ + case 3 : this.#matching[0]++; break; + case 4 : this.#matching[1]++; break; + case 6 : this.#matching[4]++; break; + } + if (match == 5 && !isHave){ + this.#matching[2]++; + } + if (match == 5 && isHave){ + this.#matching[3]++; + } + } + + #compareLotto() { + for (let lotto of this.#myLotto.getMyLottos()){ + let match = lotto.getNumbers().filter(element => this.#winningLotto.getNumbers().includes(element)); + this.#updateMatch(match.length, lotto.getNumbers().includes(this.#winningLotto.getBonusNumber())); + } + } + + calculateRateResult() { + let purchaseAmount = this.#myLotto.getLottoSize() * 1000; + let reward = this.#matching.reduce((sum, value, index) => { + return sum + (value * this.#matchReward[index]); + }, 0); + + return (reward/purchaseAmount).toFixed(1); + } + + getMatching() { + return this.#matching; + } + + +} + +export default LottoResult; \ No newline at end of file diff --git a/src/domain/MyLotto.js b/src/domain/MyLotto.js new file mode 100644 index 0000000000..01ded3557b --- /dev/null +++ b/src/domain/MyLotto.js @@ -0,0 +1,28 @@ +import Lotto from "./Lotto.js"; +import lottoNumberGenerator from "../util/lotto-number-generator.js"; + +class MyLotto { + #lottos; + + constructor(purchaseAmount) { + this.#lottos = this.#generate(purchaseAmount); + } + + #generate(purchaseAmount) { + let generator = []; + for (let i = 0; i < purchaseAmount; i++) { + generator.push(new Lotto(lottoNumberGenerator())); + } + return generator; + } + + getMyLottos() { + return this.#lottos; + } + + getLottoSize() { + return this.#lottos.length; + } +} + +export default MyLotto; \ No newline at end of file diff --git a/src/domain/WinningLotto.js b/src/domain/WinningLotto.js new file mode 100644 index 0000000000..1ad19c3223 --- /dev/null +++ b/src/domain/WinningLotto.js @@ -0,0 +1,24 @@ +import Lotto from "./Lotto.js"; +import LottoBonusValidator from "../validator/lotto-bonus-validator.js"; + +class WinningLotto extends Lotto { + #bonusNumber; + + constructor(winningNumbers, bonusNumber) { + super(winningNumbers); + this.#bonusValidate(bonusNumber); + this.#bonusNumber = bonusNumber; + } + + #bonusValidate(bonusNumber){ + LottoBonusValidator.bonusLengthValidation(bonusNumber); + LottoBonusValidator.bonusRangeValidation(bonusNumber); + LottoBonusValidator.bonusTypeValidation(bonusNumber[0]); + } + + getBonusNumber() { + return this.#bonusNumber[0]; + } +} + +export default WinningLotto; \ No newline at end of file diff --git a/src/util/lotto-number-generator.js b/src/util/lotto-number-generator.js new file mode 100644 index 0000000000..f6aa08980d --- /dev/null +++ b/src/util/lotto-number-generator.js @@ -0,0 +1,12 @@ +import { Random } from "@woowacourse/mission-utils"; + +const randomNumberGenerator = () => { + return Random.pickUniqueNumbersInRange(1, 45, 6); +}; + +const lottoNumberGenerator = () => { + const lottoNumbers = randomNumberGenerator(); + return lottoNumbers.sort((a, b) => a - b); +}; + +export default lottoNumberGenerator; \ No newline at end of file diff --git a/src/validator/input-validator.js b/src/validator/input-validator.js new file mode 100644 index 0000000000..eb0d25cb1e --- /dev/null +++ b/src/validator/input-validator.js @@ -0,0 +1,17 @@ +import { ERROR_MESSAGE } from "../constants/constants.js"; + +class InputValidator { + static inputPurchaseValidation(price) { + if (!Number.isInteger(price / 1000)) { + throw new Error(ERROR_MESSAGE.purchaseError); + } + } + + static purchaseRangeValidation(price) { + if (price <= 0) { + throw new Error(ERROR_MESSAGE.purchaseRangeError); + } + } +} + +export default InputValidator; diff --git a/src/validator/lotto-bonus-validator.js b/src/validator/lotto-bonus-validator.js new file mode 100644 index 0000000000..e271a4856b --- /dev/null +++ b/src/validator/lotto-bonus-validator.js @@ -0,0 +1,25 @@ +import { ERROR_MESSAGE } from "../constants/constants.js"; + +class LottoBonusValidator { + static bonusLengthValidation(number) { + if (number.length !== 1) { + throw new Error(ERROR_MESSAGE.bonusLengthError); + } + } + + static bonusRangeValidation(number) { + const isOutOfRange = number < 1 || number > 45; + if (isOutOfRange) { + throw new Error(ERROR_MESSAGE.lottoRangeError); + } + } + + static bonusTypeValidation(number) { + const isTypeValid = Number.isInteger(number); + if (!isTypeValid) { + throw new Error(ERROR_MESSAGE.lottoTypeError); + } + } +} + +export default LottoBonusValidator; diff --git a/src/validator/lotto-validator.js b/src/validator/lotto-validator.js new file mode 100644 index 0000000000..13462417c8 --- /dev/null +++ b/src/validator/lotto-validator.js @@ -0,0 +1,38 @@ +import { ERROR_MESSAGE } from "../constants/constants.js"; + +class LottoValidator { + static lottoLengthValidation(numbers) { + if (numbers.length !== 6) { + throw new Error(ERROR_MESSAGE.lottoLengthError); + } + } + + static lottoDuplicatedValidation(numbers) { + const isDuplicated = numbers.some(function(x) { + return numbers.indexOf(x) !== numbers.lastIndexOf(x); + }); + if (isDuplicated) { + throw new Error(ERROR_MESSAGE.lottoDuplicatedError); + } + } + + static lottoRangeValidation(numbers) { + const isOutOfRange = numbers.some(function(number) { + return number < 1 || number > 45; + }); + if (isOutOfRange) { + throw new Error(ERROR_MESSAGE.lottoRangeError); + } + } + + static lottoTypeValidation(numbers) { + const isTypeValid = numbers.every((element) => { + return Number.isInteger(element); + }); + if (!isTypeValid) { + throw new Error(ERROR_MESSAGE.lottoTypeError); + } + } +} + +export default LottoValidator; diff --git a/src/view/input-view.js b/src/view/input-view.js new file mode 100644 index 0000000000..350028fc49 --- /dev/null +++ b/src/view/input-view.js @@ -0,0 +1,40 @@ +import { Console } from "@woowacourse/mission-utils"; +import { INPUT_MESSAGE } from "../constants/constants.js"; +import InputValidator from "../validator/input-validator.js"; +import WinningLotto from "../domain/WinningLotto.js"; + +class InputView { + async readPurchaseAmount() { + try{ + const inputPrice = await Console.readLineAsync(INPUT_MESSAGE.inputPurchaseAmount); + + InputValidator.purchaseRangeValidation(inputPrice); + InputValidator.inputPurchaseValidation(inputPrice); + return inputPrice / 1000; + } + catch(error){ + Console.print(error); + await this.readPurchaseAmount(); + } + } + + async readWinningLotto() { + try{ + const readWinningNumber = await Console.readLineAsync(INPUT_MESSAGE.inputWinningNumbers); + const winningNumbers = readWinningNumber.split(",").map(Number); + + const readBonusNumber = await Console.readLineAsync(INPUT_MESSAGE.inputBonusNumber); + const bonusNumber = readBonusNumber.split(",").map(Number); + + const winningLotto = new WinningLotto(winningNumbers, bonusNumber); + + return winningLotto; + } + catch(error){ + Console.print(error); + await this.readWinningLotto(); + } + } +} + +export default InputView; \ No newline at end of file diff --git a/src/view/output-view.js b/src/view/output-view.js new file mode 100644 index 0000000000..bdeb10b40b --- /dev/null +++ b/src/view/output-view.js @@ -0,0 +1,28 @@ +import { OUTPUT_MESSAGE } from "../constants/constants.js"; +import LottoResult from "../domain/LottoResult.js"; +import { Console } from "@woowacourse/mission-utils"; + + +class OutputView { + printMyLotto(perchaseNumber, myLotto) { + Console.print(OUTPUT_MESSAGE.printPurchaseNumber(perchaseNumber)); + + for (let element of myLotto.getMyLottos()) { + Console.print(element.getNumbers()); + } + } + + printResult(myLotto, winningLotto) { + let lottoResult = new LottoResult(myLotto, winningLotto); + let match = lottoResult.getMatching(); + Console.print(OUTPUT_MESSAGE.printWinningStatistics); + Console.print(OUTPUT_MESSAGE.printFifth(match[0])); + Console.print(OUTPUT_MESSAGE.printFourth(match[1])); + Console.print(OUTPUT_MESSAGE.printThird(match[2])); + Console.print(OUTPUT_MESSAGE.printSecond(match[3])); + Console.print(OUTPUT_MESSAGE.printFirst(match[4])); + Console.print(OUTPUT_MESSAGE.printRateReturn(lottoResult.calculateRateResult())); + } +}; + +export default OutputView; \ No newline at end of file