diff --git a/README.md b/README.md index 15bb106b5..0a5871271 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ # javascript-lotto-precourse + +### 랜덤 + +- [x] 숫자의 범위 1~45 중 랜덤 + +### 입력 + +- [x] 로또 구입 금액 + +- [x] 1000원 단위로 안떨어지면 예외 + +- [x] 당첨 번호 입력 쉼표(,)를 기준으로 + +- [x] 보너스 번호 입력 + +### 출력 + +- [x] 발행한 로또 수 만큼 번호 출력 + +- [x] 로또 번호 오름 차순 + +- [x] 당첨 내역 출력 + +- [x] 수익률( 소수점 둘째 자리 반올림) + +### 예외 + +- [x]1000원 단위로 안떨어질때 + +- [x] 로또 입력 번호가 1~45가 아닐시 + +- [x] 보너스 번호 하나 이상일시 + +- [x] 로또 번호에 문자 입력 시 diff --git a/src/App.js b/src/App.js index 091aa0a5d..df8cc54ac 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import LottoController from "../src/controller/Controller.js"; + class App { - async run() {} + async run() { + const lottoController = new LottoController(); + await lottoController.start(); // 전체 애플리케이션 시작 + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..c3340eb5f 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -10,9 +10,19 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } + const isInRange = numbers.every((num) => num >= 1 && num <= 45); + if (!isInRange) { + throw new Error("[ERROR] 로또 번호는 1부터 45 사이여야 합니다."); + } + const hasDuplicates = new Set(numbers).size !== numbers.length; + if (hasDuplicates) { + throw new Error("[ERROR] 로또 번호는 중복되지 않아야 합니다."); + } } - // TODO: 추가 기능 구현 + getNumbers() { + return this.#numbers; + } } export default Lotto; diff --git a/src/controller/Controller.js b/src/controller/Controller.js new file mode 100644 index 000000000..0f4d800f5 --- /dev/null +++ b/src/controller/Controller.js @@ -0,0 +1,125 @@ +import InputView from "../view/InputView.js"; +import OutputView from "../view/OutputView.js"; +import Lotto from "../Lotto.js"; +import WinningLotto from "../model/WinningLotto.js"; +import LottoRandom from "../model/LottoRandom.js"; +import WinningResult from "../model/WinningResult.js"; + +class LottoController { + constructor() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + this.lottoRandom = new LottoRandom(); + this.winningResult = new WinningResult(); + this.lottos = []; + this.purchaseAmount = 0; + this.bonusNumber = 0; + } + + async start() { + try { + await this.setPurchaseAmount(); + await this.generateLottos(); + const winningNumbers = await this.getWinningNumbers(); + this.bonusNumber = await this.getBonusNumber(); + const result = await this.calculateResults(winningNumbers); + this.displayResults(result); + } catch (error) { + this.outputView.printError(error.message); + this.start(); + } + } + + async setPurchaseAmount() { + const purchaseAmount = await this.inputView.getPurchaseAmount(); + this.purchaseAmount = this.validatePurchaseAmount(Number(purchaseAmount)); + } + + validatePurchaseAmount(amount) { + if (isNaN(amount) || amount % 1000 !== 0) { + throw new Error("[ERROR] 구입 금액은 1,000원 단위의 숫자여야 합니다."); + } + return amount; + } + + async generateLottos() { + const purchaseCount = this.purchaseAmount / 1000; + const lottoNumbers = await this.lottoRandom.lottoRandomNumber( + purchaseCount + ); + this.lottos = lottoNumbers.map((numbers) => + new Lotto(numbers).getNumbers() + ); + this.outputView.outputLottoNumbers(purchaseCount, this.lottos); + } + + async getWinningNumbers() { + const winningInput = await this.inputView.getWinningNumbers(); + const winningNumbers = await new WinningLotto().winningnumbers( + winningInput + ); + return new Lotto(winningNumbers).getNumbers(); + } + + async getBonusNumber() { + const bonusInput = await this.inputView.getBonusNumber(); + return this.validateBonusNumber(Number(bonusInput)); + } + + validateBonusNumber(number) { + if (isNaN(number) || number < 1 || number > 45) { + throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + return number; + } + + async calculateResults(winningNumbers) { + const countMap = { 3: 0, 4: 0, 5: 0, "5+bonus": 0, 6: 0 }; + + this.lottos.forEach((lotto) => { + const matchCount = lotto.filter((num) => + winningNumbers.includes(num) + ).length; + if (matchCount === 6) { + countMap[6] += 1; // 1등 + } else if (matchCount === 5 && lotto.includes(this.bonusNumber)) { + countMap["5+bonus"] += 1; // 2등 + } else if (countMap[matchCount] !== undefined) { + countMap[matchCount] += 1; + } + }); + + const totalPrize = this.calculateTotalPrize(countMap); + const profitRate = this.calculateRateOfReturn( + totalPrize, + this.purchaseAmount + ); + return { countMap, profitRate }; + } + + calculateRateOfReturn(totalPrize, purchaseAmount) { + const rate = (totalPrize / purchaseAmount) * 100; + return parseFloat(rate.toFixed(2)); + } + + calculateTotalPrize(countMap) { + const prizeMap = { + 3: 5000, + 4: 50000, + 5: 1500000, + "5+bonus": 30000000, + 6: 2000000000, + }; + return Object.entries(countMap).reduce( + (total, [key, frequency]) => total + (prizeMap[key] || 0) * frequency, + 0 + ); + } + + displayResults({ countMap, profitRate }) { + this.outputView.outputWinningResult(countMap); + this.outputView.outputProfitRate(profitRate); + } +} + +export default LottoController; diff --git a/src/model/BuyLotto.js b/src/model/BuyLotto.js new file mode 100644 index 000000000..f3c8cd30c --- /dev/null +++ b/src/model/BuyLotto.js @@ -0,0 +1,8 @@ +class BuyLotto { + async buylottonumbers(totalPrize) { + const purchaseAmount = totalPrize / 1000; + return purchaseAmount; + } +} + +export default BuyLotto; diff --git a/src/model/LottoRandom.js b/src/model/LottoRandom.js new file mode 100644 index 000000000..08efd5df0 --- /dev/null +++ b/src/model/LottoRandom.js @@ -0,0 +1,13 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; + +class LottoRandom { + async lottoRandomNumber(purchaseAmount) { + const lottoNumbers = Array.from({ length: purchaseAmount }, () => { + const numbers = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6); + return numbers.sort((a, b) => a - b); + }); + return lottoNumbers; + } +} + +export default LottoRandom; diff --git a/src/model/WinningLotto.js b/src/model/WinningLotto.js new file mode 100644 index 000000000..956abcc11 --- /dev/null +++ b/src/model/WinningLotto.js @@ -0,0 +1,10 @@ +class WinningLotto { + async winningnumbers(winningNumbersString) { + const winningNumbers = winningNumbersString + .split(",") + .map((num) => Number(num.trim())); + return winningNumbers; + } +} + +export default WinningLotto; diff --git a/src/model/WinningResult.js b/src/model/WinningResult.js new file mode 100644 index 000000000..00e4a883e --- /dev/null +++ b/src/model/WinningResult.js @@ -0,0 +1,20 @@ +class WinningResult { + async result(winningNumbers, lottos, bonusNumber) { + const countMap = { 3: 0, 4: 0, 5: 0, "5+bonus": 0, 6: 0 }; + + lottos.forEach((lotto) => { + const matchCount = lotto.filter((num) => + winningNumbers.includes(num) + ).length; + if (matchCount === 5 && lotto.includes(bonusNumber)) { + countMap["5+bonus"] += 1; + } else if (countMap[matchCount] !== undefined) { + countMap[matchCount] += 1; + } + }); + + return countMap; + } +} + +export default WinningResult; diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 000000000..e85150062 --- /dev/null +++ b/src/view/InputView.js @@ -0,0 +1,17 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; + +class InputView { + async getPurchaseAmount() { + return MissionUtils.Console.readLineAsync("구입금액을 입력해 주세요.\n"); + } + + async getWinningNumbers() { + return MissionUtils.Console.readLineAsync("당첨 번호를 입력해주세요.\n"); + } + + async getBonusNumber() { + return MissionUtils.Console.readLineAsync("보너스 번호를 입력해주세요.\n"); + } +} + +export default InputView; diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 000000000..99c04e86b --- /dev/null +++ b/src/view/OutputView.js @@ -0,0 +1,35 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; + +class OutputView { + async outputLottoNumbers(purchase, lottoNumbers) { + MissionUtils.Console.print( + `${purchase}개를 구매했습니다.\n` + + lottoNumbers.map((numbers) => `[${numbers.join(", ")}]`).join("\n") + ); + } + + async outputWinningResult(countMap) { + MissionUtils.Console.print("당첨 통계\n---"); + MissionUtils.Console.print(`3개 일치 (5,000원) - ${countMap[3] || 0}개`); + MissionUtils.Console.print(`4개 일치 (50,000원) - ${countMap[4] || 0}개`); + MissionUtils.Console.print( + `5개 일치 (1,500,000원) - ${countMap[5] || 0}개` + ); + MissionUtils.Console.print( + `5개 일치, 보너스 볼 일치 (30,000,000원) - ${countMap["5+bonus"] || 0}개` + ); + MissionUtils.Console.print( + `6개 일치 (2,000,000,000원) - ${countMap[6] || 0}개` + ); + } + + async outputProfitRate(profitRate) { + MissionUtils.Console.print(`총 수익률은 ${profitRate}%입니다.`); + } + + printError(errorMessage) { + MissionUtils.Console.print(errorMessage); + } +} + +export default OutputView;