diff --git a/README.md b/README.md index 15bb106b5..9f3e2d799 100644 --- a/README.md +++ b/README.md @@ -1 +1,114 @@ -# javascript-lotto-precourse +# ๋กœ๋˜ (javascript-lotto-precourse) + +## ๐Ÿฅ ๊ธฐ๋Šฅ ์š”๊ตฌ ์‚ฌํ•ญ +์ดˆ๊ฐ„๋‹จ ์ž๋™์ฐจ ๊ฒฝ์ฃผ ๊ฒŒ์ž„์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +- [ ] ๋กœ๋˜ ๋ฒˆํ˜ธ์˜ ์ˆซ์ž ๋ฒ”์œ„๋Š” 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์›์ด๋‹ค. + +- [ ] ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค. + +- [ ] ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ๋งคํ•œ ๋กœ๋˜ ๋ฒˆํ˜ธ์™€ ๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ๋น„๊ตํ•˜์—ฌ ๋‹น์ฒจ ๋‚ด์—ญ ๋ฐ ์ˆ˜์ต๋ฅ ์„ ์ถœ๋ ฅํ•˜๊ณ  ๋กœ๋˜ ๊ฒŒ์ž„์„ ์ข…๋ฃŒํ•œ๋‹ค. + +- ์‚ฌ์šฉ์ž๊ฐ€ ์ž˜๋ชป๋œ ๊ฐ’์„ ์ž…๋ ฅํ•  ๊ฒฝ์šฐ "[ERROR]"๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ Error๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  ํ•ด๋‹น ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•œ ๋‹ค์Œ ํ•ด๋‹น ์ง€์ ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ž…๋ ฅ์„ ๋ฐ›๋Š”๋‹ค. + - [ ] ์ˆซ์ž ๋ฒ”์œ„๊ฐ€ 1~45๋ฅผ ๋„˜์–ด๊ฐ€๊ฒŒ ์ž…๋ ฅํ•œ ๊ฒฝ์šฐ + - [ ] ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž๋ฅผ ์ž…๋ ฅํ•œ ๊ฒฝ์šฐ + - [ ] ๊ตฌ์ž… ๊ธˆ์•ก์ด 1,000์›์œผ๋กœ ๋‚˜๋ˆ„์–ด ๋–จ์–ด์ง€์ง€ ์•Š๋Š” ๊ฒฝ์šฐ + - [ ] ์ค‘๋ณต๋œ ์ˆซ์ž๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ + - [ ] ๋กœ๋˜ ๋ฒˆํ˜ธ๋ฅผ 6๊ฐœ ์ด์ƒ ์ž…๋ ฅํ•œ ๊ฒฝ์šฐ + + +## ๐Ÿฅ ์ž…์ถœ๋ ฅ ์š”๊ตฌ ์‚ฌํ•ญ +- ์ž…๋ ฅ + - ๋กœ๋˜ ๊ตฌ์ž… ๊ธˆ์•ก์„ ์ž…๋ ฅ ๋ฐ›๋Š”๋‹ค. ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ๋‹จ์œ„๋กœ ์ž…๋ ฅ ๋ฐ›์œผ๋ฉฐ 1,000์›์œผ๋กœ ๋‚˜๋ˆ„์–ด ๋–จ์–ด์ง€์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•œ๋‹ค. + ```jsx + 14000 + ``` + + - ๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ ๋ฐ›๋Š”๋‹ค. ๋ฒˆํ˜ธ๋Š” ์‰ผํ‘œ(,)๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค. + ```jsx + 1,2,3,4,5,6 + ``` + + - ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ ๋ฐ›๋Š”๋‹ค. + ```jsx + 7 + ``` + +- ์ถœ๋ ฅ + - ๋ฐœํ–‰ํ•œ ๋กœ๋˜ ์ˆ˜๋Ÿ‰ ๋ฐ ๋ฒˆํ˜ธ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค. ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์—ฌ ๋ณด์—ฌ์ค€๋‹ค. + ```jsx + 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] + ``` + + - ๋‹น์ฒจ ๋‚ด์—ญ์„ ์ถœ๋ ฅํ•œ๋‹ค. + ```jsx + 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%) + ```jsx + ์ด ์ˆ˜์ต๋ฅ ์€ 62.5%์ž…๋‹ˆ๋‹ค. + ``` + + - ์ˆ˜์ต๋ฅ ์€ ์†Œ์ˆ˜์  ๋‘˜์งธ ์ž๋ฆฌ์—์„œ ๋ฐ˜์˜ฌ๋ฆผํ•œ๋‹ค. (ex. 100.0%, 51.5%, 1,000,000.0%) + ```jsx + [ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. + ``` + +- ์‹คํ–‰ ๊ฒฐ๊ณผ ์˜ˆ์‹œ + ```jsx + ๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”. + 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 diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..b0828dbac 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -15,4 +15,18 @@ describe("๋กœ๋˜ ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ", () => { }); // TODO: ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ + test("๋กœ๋˜ ๋ฒˆํ˜ธ์— ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฐ’์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, "a"]); + }).toThrow("[ERROR]"); + expect(() => { + new Lotto([1, 2, 3, 4, 5, null]); + }).toThrow("[ERROR]"); + }); + + test("๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ 6๊ฐœ์˜ ๊ณ ์œ ํ•œ ์ˆซ์ž๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6]); + }).not.toThrow(); + }); }); diff --git a/src/App.js b/src/App.js index 091aa0a5d..9ee413c74 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,78 @@ +import ConsoleUtil from "./utils/ConsoleUtil.js"; +import LottoManager from "./LottoManager.js"; +import errorMessages from "./errors/errorMessages.js"; +import ResultPrinter from "./ResultPrinter.js"; + class App { - async run() {} + constructor() { + this.lottoManager = new LottoManager(); + } + + async run() { + try { + const amount = await ConsoleUtil.readLine("๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n"); + this.#validateAmount(amount); + const lottoCount = Number(amount) / 1000; + const lottos = this.lottoManager.generateLottos(lottoCount); + + ConsoleUtil.print(`${lottoCount}๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.`); + lottos.forEach((lotto) => ConsoleUtil.print(`[${lotto.getNumbers().join(", ")}]`)); + + await this.#getWinningNumbers(lottoCount); + } catch (error) { + ConsoleUtil.print(error.message); + await this.run(); + } + } + + #validateAmount(amount) { + const num = Number(amount); + if (isNaN(num) || num % 1000 !== 0) { + throw new Error(errorMessages.INVALID_MONEY_ERROR); + } + return num; + } + + async #getWinningNumbers(lottoCount) { + try { + const winningNumbersInput = await ConsoleUtil.readLine("๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n"); + const winningNumbers = this.#parseNumbers(winningNumbersInput); + + const bonusNumberInput = await ConsoleUtil.readLine("๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n"); + const bonusNumber = this.#parseBonusNumber(bonusNumberInput, winningNumbers); + + const results = this.lottoManager.calculateResults(winningNumbers, bonusNumber); + ResultPrinter.printResults(results, lottoCount); + } catch (error) { + ConsoleUtil.print(error.message); + await this.#getWinningNumbers(lottoCount); + } + } + + #parseNumbers(input) { + const numbers = input.split(",").map(Number); + if (numbers.length !== 6) { + throw new Error(errorMessages.AMOUNT_OVER_ERROR); + } + if (new Set(numbers).size !== 6) { + throw new Error(errorMessages.SAME_NUMBER_ERROR); + } + if (numbers.some((num) => isNaN(num))) { + throw new Error(errorMessages.NOT_NUMBER_ERROR); + } + if (numbers.some((num) => num < 1 || num > 45)) { + throw new Error(errorMessages.RANGE_OVER_ERROR); + } + return numbers; + } + + #parseBonusNumber(input, winningNumbers) { + const bonusNumber = Number(input); + if (isNaN(bonusNumber) || bonusNumber < 1 || bonusNumber > 45 || winningNumbers.includes(bonusNumber)) { + throw new Error(errorMessages.RANGE_OVER_ERROR); + } + return bonusNumber; + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..8353612dd 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,3 +1,5 @@ +import errorMessages from "./errors/errorMessages.js"; + class Lotto { #numbers; @@ -8,11 +10,22 @@ class Lotto { #validate(numbers) { if (numbers.length !== 6) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new Error(errorMessages.AMOUNT_OVER_ERROR); + } + if (new Set(numbers).size !== 6) { + throw new Error(errorMessages.SAME_NUMBER_ERROR); + } + if (numbers.some((num) => isNaN(num))) { + throw new Error(errorMessages.NOT_NUMBER_ERROR); + } + if (numbers.some((num) => num < 1 || num > 45)) { + throw new Error(errorMessages.RANGE_OVER_ERROR); } } - // TODO: ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ + getNumbers() { + return [...this.#numbers].sort((a, b) => a - b); + } } export default Lotto; diff --git a/src/LottoManager.js b/src/LottoManager.js new file mode 100644 index 000000000..52c416c96 --- /dev/null +++ b/src/LottoManager.js @@ -0,0 +1,39 @@ +import { Random } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; + +class LottoManager { + constructor() { + this.lottos = []; + } + + generateLottos(count) { + this.lottos = Array.from({ length: count }, () => { + const numbers = Random.pickUniqueNumbersInRange(1, 45, 6); + return new Lotto(numbers); + }); + return this.lottos; + } + + calculateResults(winningNumbers, bonusNumber) { + const results = { 3: 0, 4: 0, 5: 0, "5_bonus": 0, 6: 0 }; + this.lottos.forEach((lotto) => { + const matchCount = lotto.getNumbers().filter((num) => winningNumbers.includes(num)).length; + const isBonusMatch = lotto.getNumbers().includes(bonusNumber); + + if (matchCount === 6) { + results[6]++; + } else if (matchCount === 5 && isBonusMatch) { + results["5_bonus"]++; + } else if (matchCount === 5) { + results[5]++; + } else if (matchCount === 4) { + results[4]++; + } else if (matchCount === 3) { + results[3]++; + } + }); + return results; + } +} + +export default LottoManager; diff --git a/src/ResultPrinter.js b/src/ResultPrinter.js new file mode 100644 index 000000000..42c3f655d --- /dev/null +++ b/src/ResultPrinter.js @@ -0,0 +1,27 @@ +import ConsoleUtil from "./utils/ConsoleUtil.js"; + +export default class ResultPrinter { + static printResults(results, lottoCount) { + const prizeMap = { + 3: { amount: 5000, label: "3๊ฐœ ์ผ์น˜ (5,000์›)" }, + 4: { amount: 50000, label: "4๊ฐœ ์ผ์น˜ (50,000์›)" }, + 5: { amount: 1500000, label: "5๊ฐœ ์ผ์น˜ (1,500,000์›)" }, + "5_bonus": { amount: 30000000, label: "5๊ฐœ ์ผ์น˜, ๋ณด๋„ˆ์Šค ๋ณผ ์ผ์น˜ (30,000,000์›)" }, + 6: { amount: 2000000000, label: "6๊ฐœ ์ผ์น˜ (2,000,000,000์›)" }, + }; + + ConsoleUtil.print("๋‹น์ฒจ ํ†ต๊ณ„\n---"); + let totalEarnings = 0; + + Object.keys(prizeMap).forEach((key) => { + const count = results[key] || 0; + const prize = prizeMap[key]; + totalEarnings += count * prize.amount; + ConsoleUtil.print(`${prize.label} - ${count}๊ฐœ`); + }); + + const investment = lottoCount * 1000; + const profitRate = ((totalEarnings / investment) * 100).toFixed(1); + ConsoleUtil.print(`์ด ์ˆ˜์ต๋ฅ ์€ ${profitRate}%์ž…๋‹ˆ๋‹ค.`); + } +} diff --git a/src/errors/errorMessages.js b/src/errors/errorMessages.js new file mode 100644 index 000000000..98217eaec --- /dev/null +++ b/src/errors/errorMessages.js @@ -0,0 +1,9 @@ +const errorMessages = { + RANGE_OVER_ERROR: "[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + NOT_NUMBER_ERROR: "[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + INVALID_MONEY_ERROR: "[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ๋‹จ์œ„๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + SAME_NUMBER_ERROR: "[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” 6๊ฐœ์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + AMOUNT_OVER_ERROR: "[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + }; + + export default errorMessages; \ No newline at end of file diff --git a/src/utils/ConsoleUtil.js b/src/utils/ConsoleUtil.js new file mode 100644 index 000000000..e9c049f2e --- /dev/null +++ b/src/utils/ConsoleUtil.js @@ -0,0 +1,12 @@ +import { Console } from "@woowacourse/mission-utils"; + +const ConsoleUtil = { + print(message) { + Console.print(message); + }, + async readLine(prompt) { + return await Console.readLineAsync(prompt); + }, +}; + +export default ConsoleUtil;