diff --git a/README.md b/README.md index 15bb106b5..ac20fe34c 100644 --- a/README.md +++ b/README.md @@ -1 +1,63 @@ # javascript-lotto-precourse + +## 구현 기능 목록 + +### 입력 + +- [ ] 사용자로부터 로또 구입 금액을 입력 받는다. + - [ ] 1,000원으로 나누어 떨어지지 않으면 예외 처리 + - [ ] 숫자가 아닌 경우 예외 처리 +- [ ] 사용자로부터 당첨 번호를 입력 받는다. + - [ ] 1 ~ 45 범위에 존재하지 않으면 예외 처리 + - [ ] 중복되는 숫자가 존재하면 예외 처리 + - [ ] 숫자가 아닌 경우 예외 처리 + - [ ] 공백을 포함한 경우 예외 처리 + - [ ] 6개의 숫자를 입력하지 않은 경우 예외 처리 +- [ ] 당첨 번호는 쉼표(,)를 기준으로 구분한다. + - [ ] 쉼표가 아닌 구분자가 존재하면 예외 처리 +- [ ] 사용자로부터 보너스 번호를 입력 받는다. + - [ ] 1 ~ 45 범위에 존재하지 않으면 예외 처리 + - [ ] 중복되는 숫자가 존재하면 예외 처리 + - [ ] 숫자가 아닌 경우 예외 처리 + +### 출력 + +- [ ] 로또 번호를 오름차순으로 정렬한다. + +- [ ] 발행한 로또 수량 및 번호를 출력한다. + ``` + 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개 + ``` + +- [ ] 수익률을 소수점 둘째 자리에서 반올림해서 출력한다. + ``` + 총 수익률은 62.5%입니다. + ``` + +- [ ] 예외 상황 시 에러 문구를 출력한다. 반드시 `[ERROR]`로 시작한다. + ``` + [ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다. + ``` + +### 로또 추첨 + +- [ ] 구입 금액에 해당하는 만큼 로또를 발행한다. +- [ ] 로또 발행 시 중복되지 않는 6개의 숫자를 뽑는다. +- [ ] 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다. \ No newline at end of file diff --git a/src/App.js b/src/App.js index 091aa0a5d..0f33cd426 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import LottoController from "./controller/LottoController.js"; + class App { - async run() {} + async run() { + const controller = new LottoController(); + await controller.start(); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js deleted file mode 100644 index cb0b1527e..000000000 --- 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/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 000000000..6caa4c468 --- /dev/null +++ b/src/controller/LottoController.js @@ -0,0 +1,25 @@ +import InputView from "../view/InputView.js"; +import OutputView from "../view/OutputView.js"; +import LottoGame from "../service/LottoGame.js"; + +class LottoController { + async start() { + try { + const purchaseAmount = await InputView.getPurchaseAmount(); + const lottoGame = new LottoGame(purchaseAmount); + OutputView.printLottoTickets(lottoGame.lottoTickets); + + const winningNumbers = await InputView.getWinningNumbers(); + const bonusNumber = await InputView.getBonusNumber(); + + const ranks = lottoGame.calculateRanks(winningNumbers, bonusNumber); + const earningRate = this.calculateEarningRate(ranks, purchaseAmount); + + OutputView.printResults(ranks, earningRate); + } catch (error) { + OutputView.printError(error.message); + } + } +} + +export default LottoController; \ No newline at end of file diff --git a/src/model/Lotto.js b/src/model/Lotto.js new file mode 100644 index 000000000..e05d51862 --- /dev/null +++ b/src/model/Lotto.js @@ -0,0 +1,39 @@ +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + if (numbers.length !== 6) { + throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + } + + if (new Set(numbers).size !== numbers.length) { + throw new Error("[ERROR] 로또 번호에 중복된 숫자가 있습니다."); + } + + if (numbers.some(num => num < 1 || num > 45)) { + throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } + + // 당첨 번호 일치 개수를 반환 + getMatchCount(winningNumbers) { + return this.#numbers.filter((num) => winningNumbers.includes(num)).length; + } + + // 보너스 번호 일치 여부 확인 + hasBonusNumber(bonusNumber) { + return this.#numbers.includes(bonusNumber); + } + + // private으로 선언된 numbers를 외부에서 확인하기 위한 Getter + get Numbers() { + return this.#numbers; + } +} + +export default Lotto; diff --git a/src/service/LottoGame.js b/src/service/LottoGame.js new file mode 100644 index 000000000..1dead5bed --- /dev/null +++ b/src/service/LottoGame.js @@ -0,0 +1,47 @@ +import Lotto from "../model/Lotto.js"; +import prizeMoney from "./data.js"; +import RandomNumberGenerator from "../util/RandomNumberGenerator.js"; + +class LottoGame { + constructor(purchaseAmount) { + this.lottoTickets = this.generateLottoTickets(purchaseAmount); + } + + generateLottoTickets(amount) { + const ticketCount = Math.floor(amount / 1000); + return Array.from({ length: ticketCount }, () => new Lotto(RandomNumberGenerator.generateLottoNumbers())); + } + + calculateRanks(winningNumbers, bonusNumber) { + const ranks = { + "3개 일치": 0, + "4개 일치": 0, + "5개 일치": 0, + "5개 일치 + 보너스 볼 일치": 0, + "6개 일치": 0, + }; + + this.lottoTickets.forEach((ticket) => { + const matchCount = ticket.getMatchCount(winningNumbers); + const hasBonus = ticket.hasBonusNumber(bonusNumber); + + if (matchCount === 3) ranks["3개 일치"]++; + if (matchCount === 4) ranks["4개 일치"]++; + if (matchCount === 5) ranks["5개 일치"]++; + if (matchCount === 5 && hasBonus) ranks["5개 일치 + 보너스 볼 일치"]++; + if (matchCount === 6) ranks["6개 일치"]++; + }); + + return ranks; + } + + calculateEarningRate(ranks, purchaseAmount) { + const totalPrize = Object.entries(ranks).reduce( + (sum, [rank, count]) => sum + prizeMoney[rank] * count, + 0 + ); + return ((totalPrize / purchaseAmount) * 100).toFixed(1); + } +} + +export default LottoGame; \ No newline at end of file diff --git a/src/service/data.js b/src/service/data.js new file mode 100644 index 000000000..8e43130f0 --- /dev/null +++ b/src/service/data.js @@ -0,0 +1,9 @@ +const prizeMoney = { + "3개 일치": 5000, + "4개 일치": 50000, + "5개 일치": 1500000, + "5개 일치 + 보너스 볼 일치": 30000000, + "6개 일치": 2000000000, +}; + +export default prizeMoney; \ No newline at end of file diff --git a/src/util/RandomNumberGenerator.js b/src/util/RandomNumberGenerator.js new file mode 100644 index 000000000..f7df3231e --- /dev/null +++ b/src/util/RandomNumberGenerator.js @@ -0,0 +1,9 @@ +import { Random } from "@woowacourse/mission-utils"; + +const RandomNumberGenerator = { + generateLottoNumbers() { + return Random.pickUniqueNumbersInRange(1, 45, 6).sort((a, b) => a - b); + }, +}; + +export default RandomNumberGenerator; \ No newline at end of file diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 000000000..68d72d627 --- /dev/null +++ b/src/view/InputView.js @@ -0,0 +1,38 @@ +import { Console } from "@woowacourse/mission-utils" + +const InputView = { + async getPurchaseAmount() { + const input = await Console.readLineAsync("구입 금액을 입력해주세요.\n"); + const amount = parseInt(input); + + if (isNaN(amount) || amount % 1000 !== 0) { + throw new Error("[ERROR] 구입 금액은 1,000원 단위여야 합니다."); + } + + return amount; + }, + + async getWinningNumbers() { + const input = await Console.readLineAsync("당첨 번호를 입력해 주세요.\n"); + const numbers = input.split(",").map(Number); + + if (numbers.length !== 6 || numbers.some((num) => isNaN(num) || num < 1 || num >45)) { + throw new Error("[ERROR] 당첨 번호는 1부터 45 사이의 숫자 6개여야 합니다."); + } + + return numbers; + }, + + async getBonusNumber() { + const input = await Console.readLineAsync("보너스 번호를 입력해 주세요.\n"); + const bonus = parseInt(input); + + if (isNaN(bonus) || bonus < 1 || bonus > 45) { + throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + + return bonus; + } +} + +export default InputView; \ No newline at end of file diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 000000000..5374f2c0e --- /dev/null +++ b/src/view/OutputView.js @@ -0,0 +1,23 @@ +import { Console } from "@woowacourse/mission-utils" +import prizeMoney from "../service/data.js"; + +const OutputView = { + printLottoTickets(tickets) { + Console.print(`\n${tickets.length}개를 구매했습니다.`); + tickets.forEach((ticket) => Console.print(`[${ticket.numbers.join(", ")}]`)); + }, + + printResults(ranks, earningRate) { + Console.print("\n당첨 통계\n---") + Object.entries(ranks).forEach(([rank, count]) => { + Console.print(`${rank} (${prizeMoney[rank].toLocaleString("ko-KR")}원) - ${count}개`) + }); + Console.print(`총 수익률은 ${earningRate}% 입니다.`); + }, + + printError(message) { + Console.print(message); + } +} + +export default OutputView; \ No newline at end of file