From 301abf6f158e03051a892945dd655a0eefca0706 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 15:17:53 +0900 Subject: [PATCH 01/18] =?UTF-8?q?docs=20:=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=AA=A9=EB=A1=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 15bb106b5..44d21f2cd 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ # javascript-lotto-precourse + +## 기능 목록 + +1. 로또 구입 및 발행 + - 구입 금액 입력 + - 로또 발행 수량 계산 + - 로또 번호 생성 + - 로또 발행 결과 출력 +2. 당첨 번호 및 보너스 번호 입력 + - 당첨 번호 입력 + - 보너스 번호 입력 +3. 당첨 확인 및 결과 출력 + - 로또 당첨 확인 + - 당첨 내역 집계 + - 당첨 통계 출력 + - 수익률 계산 및 출력 From a930ae87bedd66a8ec6808b55381cec1d298c98c Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 16:58:12 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat=20:=20=EB=A1=9C=EB=98=90=20=EB=B0=9C?= =?UTF-8?q?=EB=A7=A4=EA=B8=B0=20=EA=B8=B0=EB=B3=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로또 구입 금액 입력부터 결과 출력까지의 전체 기능을 구현, 당첨 번호/보너스 번호 입력 및 통계 처리 로직 포함 --- src/App.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5d..4cc38cde9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,74 @@ +import { Console, Random } from "@woowacourse/mission-utils"; class App { - async run() {} + async run() { + const purchaseAmount = await Console.readLineAsync( + "구입 금액을 입력해 주세요: " + ); + const count = Math.floor(Number(purchaseAmount) / 1000); + Console.print(`${count}개를 구매했습니다.`); + const winningNumbers = await Console.readLineAsync( + "당첨 번호를 입력해주세요.: " + ); + const bonusNumber = await Console.readLineAsync( + "보너스 번호를 입력해주세요: " + ); + let statistics = { + "3개 일치": 0, + "4개 일치": 0, + "5개 일치": 0, + "5개 일치, 보너스 볼 일치": 0, + "6개 일치": 0, + }; + const winningAmounts = { + "3개 일치": 5000, + "4개 일치": 50000, + "5개 일치": 1500000, + "5개 일치, 보너스 볼 일치": 30000000, + "6개 일치": 2000000000, + }; + for (let i = 0; i < count; i++) { + const randomNumbers = Random.pickUniqueNumbersInRange(1, 45, 6); + Console.print(`[${randomNumbers.join(", ")}]`); + + for (const number of randomNumbers) { + let cnt = 0; + let bonusCnt = 0; + if (winningNumbers.includes(number)) { + cnt++; + } + if (bonusNumber.includes(number)) { + bonusCnt++; + } + if (cnt === 3) { + statistics["3개 일치"] += 1; + } else if (cnt === 4) { + statistics["4개 일치"] += 1; + } else if (cnt === 5) { + statistics["5개 일치"] += 1; + } else if (cnt === 5 && bonusCnt === 1) { + statistics["5개 일치, 보너스 볼 일치"] += 1; + } else if (cnt === 6) { + statistics["6개 일치"] += 1; + } + } + } + + let totalEarning = 0; + for (let key in statistics) { + if (statistics[key] !== 0) { + totalEarning += statistics[key] * winningAmounts[key]; + } + } + + const rateOfReturn = (totalEarning / purchaseAmount) * 100; + + Console.print("당첨 통계"); + Console.print("---"); + for (let key in statistics) { + Console.print(`${key} (${winningAmounts[key]}원) - ${statistics[key]}개`); + } + Console.print(`총 수익률은 ${rateOfReturn}%입니다.`); + } } export default App; From fcfe2c76254630b5ee257b7b209a6eb8c3fc29ac Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 17:35:58 +0900 Subject: [PATCH 03/18] =?UTF-8?q?fix=20:=20=EB=B0=B0=EC=97=B4=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit statistics 조건 불일치 문제, cnt==6부터 해서 해결 --- src/App.js | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/App.js b/src/App.js index 4cc38cde9..f02f431df 100644 --- a/src/App.js +++ b/src/App.js @@ -2,16 +2,29 @@ import { Console, Random } from "@woowacourse/mission-utils"; class App { async run() { const purchaseAmount = await Console.readLineAsync( - "구입 금액을 입력해 주세요: " + "구입 금액을 입력해 주세요.\n" ); const count = Math.floor(Number(purchaseAmount) / 1000); - Console.print(`${count}개를 구매했습니다.`); + Console.print(`\n${count}개를 구매했습니다.`); + const lottoNumbers = []; + for (let i = 0; i < count; i++) { + const randomNumbers = Random.pickUniqueNumbersInRange(1, 45, 6); + lottoNumbers.push(randomNumbers); + Console.print(`[${randomNumbers.join(", ")}]`); + } + const winningNumbers = await Console.readLineAsync( "당첨 번호를 입력해주세요.: " ); const bonusNumber = await Console.readLineAsync( "보너스 번호를 입력해주세요: " ); + // 당첨 번호 및 보너스 번호 Number 배열로 변환 + const winningNumbersArray = winningNumbers + .split(",") + .map((num) => Number(num.trim())); + const bonusNum = Number(bonusNumber.trim()); + let statistics = { "3개 일치": 0, "4개 일치": 0, @@ -26,30 +39,29 @@ class App { "5개 일치, 보너스 볼 일치": 30000000, "6개 일치": 2000000000, }; - for (let i = 0; i < count; i++) { - const randomNumbers = Random.pickUniqueNumbersInRange(1, 45, 6); - Console.print(`[${randomNumbers.join(", ")}]`); - for (const number of randomNumbers) { - let cnt = 0; - let bonusCnt = 0; - if (winningNumbers.includes(number)) { + for (const ticket of lottoNumbers) { + let cnt = 0; + let bonusCnt = 0; + for (const number of ticket) { + if (winningNumbersArray.includes(number)) { cnt++; } - if (bonusNumber.includes(number)) { + if (number === bonusNum) { bonusCnt++; } - if (cnt === 3) { - statistics["3개 일치"] += 1; - } else if (cnt === 4) { - statistics["4개 일치"] += 1; - } else if (cnt === 5) { - statistics["5개 일치"] += 1; - } else if (cnt === 5 && bonusCnt === 1) { - statistics["5개 일치, 보너스 볼 일치"] += 1; - } else if (cnt === 6) { - statistics["6개 일치"] += 1; - } + } + // 순서 변경 6개 일치부터 검사 + if (cnt === 6) { + statistics["6개 일치"] += 1; + } else if (cnt === 5 && bonusCnt === 1) { + statistics["5개 일치, 보너스 볼 일치"] += 1; + } else if (cnt === 5) { + statistics["5개 일치"] += 1; + } else if (cnt === 4) { + statistics["4개 일치"] += 1; + } else if (cnt === 3) { + statistics["3개 일치"] += 1; } } From dbd636411c5889eaf739c412d8832eff03f4e08f Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 17:42:41 +0900 Subject: [PATCH 04/18] =?UTF-8?q?fix=20:=20=EB=9E=9C=EB=8D=A4=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=20=EC=98=A4=EB=A6=84=20=EC=B0=A8=EC=88=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=EB=A1=9C=20=EC=88=98=EC=A0=95,=20=EC=88=98=EC=9D=B5?= =?UTF-8?q?=EB=A5=A0=20=EC=86=8C=EC=88=AB=EC=A0=90=20=EC=B2=AB=EB=B2=88?= =?UTF-8?q?=EC=A7=B8=20=EC=9E=90=EB=A6=AC=EA=B9=8C=EC=A7=80=EB=A1=9C=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 --- src/App.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/App.js b/src/App.js index f02f431df..e7bac9ba2 100644 --- a/src/App.js +++ b/src/App.js @@ -8,16 +8,19 @@ class App { Console.print(`\n${count}개를 구매했습니다.`); const lottoNumbers = []; for (let i = 0; i < count; i++) { - const randomNumbers = Random.pickUniqueNumbersInRange(1, 45, 6); + // 오름차순 정렬 후 배열에 추가 + const randomNumbers = Random.pickUniqueNumbersInRange(1, 45, 6).sort( + (a, b) => a - b + ); lottoNumbers.push(randomNumbers); Console.print(`[${randomNumbers.join(", ")}]`); } const winningNumbers = await Console.readLineAsync( - "당첨 번호를 입력해주세요.: " + "당첨 번호를 입력해주세요.\n" ); const bonusNumber = await Console.readLineAsync( - "보너스 번호를 입력해주세요: " + "보너스 번호를 입력해주세요.\n" ); // 당첨 번호 및 보너스 번호 Number 배열로 변환 const winningNumbersArray = winningNumbers @@ -71,8 +74,8 @@ class App { totalEarning += statistics[key] * winningAmounts[key]; } } - - const rateOfReturn = (totalEarning / purchaseAmount) * 100; + // 소수점 첫째자리 까지 표현 + const rateOfReturn = ((totalEarning / purchaseAmount) * 100).toFixed(1); Console.print("당첨 통계"); Console.print("---"); From 211c4933b5ecf51ec19a1e23ef0629a65191df22 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:11:14 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20InputView=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/InputView.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/InputView.js diff --git a/src/InputView.js b/src/InputView.js new file mode 100644 index 000000000..e14f567b3 --- /dev/null +++ b/src/InputView.js @@ -0,0 +1,25 @@ +import { Console } from "@woowacourse/mission-utils"; +class InputView { + async purchaseAmountInput() { + const purchaseAmount = await Console.readLineAsync( + "구입 금액을 입력해 주세요.\n" + ); + return Number(purchaseAmount); + } + + async winningNumbersInput() { + const winningNumbers = await Console.readLineAsync( + "당첨 번호를 입력해주세요.\n" + ); + return winningNumbers.split(",").map((num) => Number(num.trim())); + } + + async bonusNumberInput() { + const bonusNumber = await Console.readLineAsync( + "보너스 번호를 입력해주세요.\n" + ); + return Number(bonusNumber.trim()); + } +} + +export default InputView; From a2d000f4f6738a3e26b02bb059e2e093dfc7cbf1 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:11:46 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat=20:=20=EB=A1=9C=EB=98=90=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20LottoMake?= =?UTF-8?q?r=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LottoMaker.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/LottoMaker.js diff --git a/src/LottoMaker.js b/src/LottoMaker.js new file mode 100644 index 000000000..31aef4e24 --- /dev/null +++ b/src/LottoMaker.js @@ -0,0 +1,13 @@ +import { Random } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; + +class LottoMaker { + static createLottoNumbers() { + const numbers = Random.pickUniqueNumbersInRange(1, 45, 6).sort( + (a, b) => a - b + ); + return new Lotto(numbers); + } +} + +export default LottoMaker; From b308d91b69ac4cd8055e580feddac2cec2f390d1 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:12:49 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat=20:=20Lotto=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EB=8B=B9=EC=B2=A8=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Lotto.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..bd59358fa 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -12,7 +12,17 @@ class Lotto { } } - // TODO: 추가 기능 구현 + getNumbers() { + return [...this.#numbers]; + } + + countMatches(winningNumbers) { + return this.#numbers.filter((num) => winningNumbers.includes(num)).length; + } + + hasBonus(bonusNumber) { + return this.#numbers.includes(bonusNumber); + } } export default Lotto; From 9f24ba0a85e2f33b4e5459e74adf216e8ece88f0 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:13:25 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat=20:=20=EB=8B=B9=EC=B2=A8=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?WinningResult=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/WinningResult.js | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/WinningResult.js diff --git a/src/WinningResult.js b/src/WinningResult.js new file mode 100644 index 000000000..dbe82f4a7 --- /dev/null +++ b/src/WinningResult.js @@ -0,0 +1,56 @@ +class WinningResult { + static PRIZE = { + THREE: { match: 3, bonus: false, prize: 5000, label: "3개 일치" }, + FOUR: { match: 4, bonus: false, prize: 50000, label: "4개 일치" }, + FIVE: { match: 5, bonus: false, prize: 1500000, label: "5개 일치" }, + FIVE_BONUS: { + match: 5, + bonus: true, + prize: 30000000, + label: "5개 일치, 보너스 볼 일치", + }, + SIX: { match: 6, bonus: false, prize: 2000000000, label: "6개 일치" }, + }; + + constructor() { + this.statistics = { + "3개 일치": 0, + "4개 일치": 0, + "5개 일치": 0, + "5개 일치, 보너스 볼 일치": 0, + "6개 일치": 0, + }; + } + + checkWinning(lotto, winningNumbers, bonusNumber) { + const matchCount = lotto.countMatches(winningNumbers); + const hasBonus = lotto.hasBonus(bonusNumber); + + if (matchCount === 6) { + this.statistics["6개 일치"]++; + } else if (matchCount === 5 && hasBonus) { + this.statistics["5개 일치, 보너스 볼 일치"]++; + } else if (matchCount === 5) { + this.statistics["5개 일치"]++; + } else if (matchCount === 4) { + this.statistics["4개 일치"]++; + } else if (matchCount === 3) { + this.statistics["3개 일치"]++; + } + } + + calculateTotalPrize() { + let total = 0; + for (const [rank, prize] of Object.entries(WinningResult.PRIZE)) { + const label = prize.label; + total += this.statistics[label] * prize.prize; + } + return total; + } + + getStatistics() { + return { ...this.statistics }; + } +} + +export default WinningResult; From 34933a25b930aee28a340c02d7552503f7681391 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:15:06 +0900 Subject: [PATCH 09/18] =?UTF-8?q?feat=20:=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20OutputVie=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/OutputView.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/OutputView.js diff --git a/src/OutputView.js b/src/OutputView.js new file mode 100644 index 000000000..8270b2e02 --- /dev/null +++ b/src/OutputView.js @@ -0,0 +1,30 @@ +import { Console } from "@woowacourse/mission-utils"; +import WinningResult from "./WinningResult.js"; + +class OutputView { + static printPurchaseResult(count) { + Console.print(`\n${count}개를 구매했습니다.`); + } + + static printLottoNumbers(lotto) { + Console.print(`[${lotto.getNumbers().join(", ")}]`); + } + + static printStatistics(statistics, totalPrize, purchaseAmount) { + Console.print("\n당첨 통계"); + Console.print("---"); + + for (const [rank, prize] of Object.entries(WinningResult.PRIZE)) { + const label = prize.label; + const count = statistics[label]; + Console.print( + `${label} (${prize.prize.toLocaleString()}원) - ${count}개` + ); + } + + const rateOfReturn = ((totalPrize / purchaseAmount) * 100).toFixed(1); + Console.print(`총 수익률은 ${rateOfReturn}%입니다.`); + } +} + +export default OutputView; From e25bacb060f92c3fa000019509d3ccb6be32e126 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:15:34 +0900 Subject: [PATCH 10/18] =?UTF-8?q?refactor=20:=20App=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EA=B0=81=20=EA=B8=B0=EB=8A=A5=EB=B3=84=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 102 +++++++++++++---------------------------------------- 1 file changed, 25 insertions(+), 77 deletions(-) diff --git a/src/App.js b/src/App.js index e7bac9ba2..8ec8be2a6 100644 --- a/src/App.js +++ b/src/App.js @@ -1,88 +1,36 @@ -import { Console, Random } from "@woowacourse/mission-utils"; +import InputView from "./InputView.js"; +import OutputView from "./OutputView.js"; +import LottoMaker from "./LottoMaker.js"; +import WinningResult from "./WinningResult.js"; class App { async run() { - const purchaseAmount = await Console.readLineAsync( - "구입 금액을 입력해 주세요.\n" - ); - const count = Math.floor(Number(purchaseAmount) / 1000); - Console.print(`\n${count}개를 구매했습니다.`); - const lottoNumbers = []; - for (let i = 0; i < count; i++) { - // 오름차순 정렬 후 배열에 추가 - const randomNumbers = Random.pickUniqueNumbersInRange(1, 45, 6).sort( - (a, b) => a - b - ); - lottoNumbers.push(randomNumbers); - Console.print(`[${randomNumbers.join(", ")}]`); - } - - const winningNumbers = await Console.readLineAsync( - "당첨 번호를 입력해주세요.\n" - ); - const bonusNumber = await Console.readLineAsync( - "보너스 번호를 입력해주세요.\n" - ); - // 당첨 번호 및 보너스 번호 Number 배열로 변환 - const winningNumbersArray = winningNumbers - .split(",") - .map((num) => Number(num.trim())); - const bonusNum = Number(bonusNumber.trim()); + const inputView = new InputView(); + const purchaseAmount = await inputView.purchaseAmountInput(); + const count = Math.floor(purchaseAmount / 1000); - let statistics = { - "3개 일치": 0, - "4개 일치": 0, - "5개 일치": 0, - "5개 일치, 보너스 볼 일치": 0, - "6개 일치": 0, - }; - const winningAmounts = { - "3개 일치": 5000, - "4개 일치": 50000, - "5개 일치": 1500000, - "5개 일치, 보너스 볼 일치": 30000000, - "6개 일치": 2000000000, - }; + OutputView.printPurchaseResult(count); - for (const ticket of lottoNumbers) { - let cnt = 0; - let bonusCnt = 0; - for (const number of ticket) { - if (winningNumbersArray.includes(number)) { - cnt++; - } - if (number === bonusNum) { - bonusCnt++; - } - } - // 순서 변경 6개 일치부터 검사 - if (cnt === 6) { - statistics["6개 일치"] += 1; - } else if (cnt === 5 && bonusCnt === 1) { - statistics["5개 일치, 보너스 볼 일치"] += 1; - } else if (cnt === 5) { - statistics["5개 일치"] += 1; - } else if (cnt === 4) { - statistics["4개 일치"] += 1; - } else if (cnt === 3) { - statistics["3개 일치"] += 1; - } + const lottos = []; + for (let i = 0; i < count; i++) { + const lotto = LottoMaker.createLottoNumbers(); + lottos.push(lotto); + OutputView.printLottoNumbers(lotto); } - let totalEarning = 0; - for (let key in statistics) { - if (statistics[key] !== 0) { - totalEarning += statistics[key] * winningAmounts[key]; - } - } - // 소수점 첫째자리 까지 표현 - const rateOfReturn = ((totalEarning / purchaseAmount) * 100).toFixed(1); + const winningNumbers = await inputView.winningNumbersInput(); + const bonusNumber = await inputView.bonusNumberInput(); - Console.print("당첨 통계"); - Console.print("---"); - for (let key in statistics) { - Console.print(`${key} (${winningAmounts[key]}원) - ${statistics[key]}개`); + const result = new WinningResult(); + for (const lotto of lottos) { + result.checkWinning(lotto, winningNumbers, bonusNumber); } - Console.print(`총 수익률은 ${rateOfReturn}%입니다.`); + + const totalPrize = result.calculateTotalPrize(); + OutputView.printStatistics( + result.getStatistics(), + totalPrize, + purchaseAmount + ); } } From 32281898af73926c239865b0c07fbd48625d2b6e Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:25:03 +0900 Subject: [PATCH 11/18] =?UTF-8?q?feat=20:=20=EB=A1=9C=EB=98=90=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B0=9C=EC=88=98,=20=EC=A4=91=EB=B3=B5,=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Lotto.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Lotto.js b/src/Lotto.js index bd59358fa..efd584e95 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -10,6 +10,16 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } + + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== 6) { + throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + + const isInvalidRange = numbers.some((num) => num < 1 || num > 45); + if (isInvalidRange) { + throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } } getNumbers() { From b7ebafcff8adbe12d7a7a4d611ed01664c79a189 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:25:31 +0900 Subject: [PATCH 12/18] =?UTF-8?q?feat=20:=20=EA=B5=AC=EC=9E=85=EA=B8=88?= =?UTF-8?q?=EC=95=A1,=20=EB=8B=B9=EC=B2=A8=EB=B2=88=ED=98=B8,=20=EB=B3=B4?= =?UTF-8?q?=EB=84=88=EC=8A=A4=20=EB=B2=88=ED=98=B8=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/InputView.js | 50 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/InputView.js b/src/InputView.js index e14f567b3..6f8e556a7 100644 --- a/src/InputView.js +++ b/src/InputView.js @@ -1,24 +1,52 @@ import { Console } from "@woowacourse/mission-utils"; + class InputView { async purchaseAmountInput() { - const purchaseAmount = await Console.readLineAsync( - "구입 금액을 입력해 주세요.\n" - ); - return Number(purchaseAmount); + const input = await Console.readLineAsync("구입 금액을 입력해 주세요.\n"); + const amount = Number(input); + + if (isNaN(amount) || amount <= 0) { + throw new Error("[ERROR] 구입 금액은 양수여야 합니다."); + } + + if (amount % 1000 !== 0) { + throw new Error("[ERROR] 구입 금액은 1,000원 단위여야 합니다."); + } + + return amount; } async winningNumbersInput() { - const winningNumbers = await Console.readLineAsync( - "당첨 번호를 입력해주세요.\n" - ); - return winningNumbers.split(",").map((num) => Number(num.trim())); + const input = await Console.readLineAsync("\n당첨 번호를 입력해주세요.\n"); + const numbers = input.split(",").map((num) => Number(num.trim())); + + if (numbers.length !== 6) { + throw new Error("[ERROR] 당첨 번호는 6개여야 합니다."); + } + + if (numbers.some((num) => isNaN(num) || num < 1 || num > 45)) { + throw new Error("[ERROR] 당첨 번호는 1부터 45 사이의 숫자여야 합니다."); + } + + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== 6) { + throw new Error("[ERROR] 당첨 번호는 중복될 수 없습니다."); + } + + return numbers; } async bonusNumberInput() { - const bonusNumber = await Console.readLineAsync( - "보너스 번호를 입력해주세요.\n" + const input = await Console.readLineAsync( + "\n보너스 번호를 입력해주세요.\n" ); - return Number(bonusNumber.trim()); + const number = Number(input.trim()); + + if (isNaN(number) || number < 1 || number > 45) { + throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + + return number; } } From e63e2b32c0333bcb09920c31f86ef77f92655597 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:26:19 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat=20App=EC=97=90=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 51 +++++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/App.js b/src/App.js index 8ec8be2a6..ddc2e2a2c 100644 --- a/src/App.js +++ b/src/App.js @@ -2,35 +2,42 @@ import InputView from "./InputView.js"; import OutputView from "./OutputView.js"; import LottoMaker from "./LottoMaker.js"; import WinningResult from "./WinningResult.js"; +import { Console } from "@woowacourse/mission-utils"; + class App { async run() { - const inputView = new InputView(); - const purchaseAmount = await inputView.purchaseAmountInput(); - const count = Math.floor(purchaseAmount / 1000); + try { + const inputView = new InputView(); - OutputView.printPurchaseResult(count); + const purchaseAmount = await inputView.purchaseAmountInput(); + const count = Math.floor(purchaseAmount / 1000); - const lottos = []; - for (let i = 0; i < count; i++) { - const lotto = LottoMaker.createLottoNumbers(); - lottos.push(lotto); - OutputView.printLottoNumbers(lotto); - } + OutputView.printPurchaseResult(count); - const winningNumbers = await inputView.winningNumbersInput(); - const bonusNumber = await inputView.bonusNumberInput(); + const lottos = []; + for (let i = 0; i < count; i++) { + const lotto = LottoMaker.createLottoNumbers(); + lottos.push(lotto); + OutputView.printLottoNumbers(lotto); + } - const result = new WinningResult(); - for (const lotto of lottos) { - result.checkWinning(lotto, winningNumbers, bonusNumber); - } + const winningNumbers = await inputView.winningNumbersInput(); + const bonusNumber = await inputView.bonusNumberInput(); - const totalPrize = result.calculateTotalPrize(); - OutputView.printStatistics( - result.getStatistics(), - totalPrize, - purchaseAmount - ); + const result = new WinningResult(); + for (const lotto of lottos) { + result.checkWinning(lotto, winningNumbers, bonusNumber); + } + + const totalPrize = result.calculateTotalPrize(); + OutputView.printStatistics( + result.getStatistics(), + totalPrize, + purchaseAmount + ); + } catch (error) { + Console.print(error.message); + } } } From b3832f2a88f46f83545d8ebc2b410ed852a95f98 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 22:33:41 +0900 Subject: [PATCH 14/18] =?UTF-8?q?docs=20:=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=82=AC=ED=95=AD=20=EB=8D=94=20=EA=B5=AC=EC=B2=B4?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 44d21f2cd..ee8791deb 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,47 @@ ## 기능 목록 -1. 로또 구입 및 발행 - - 구입 금액 입력 - - 로또 발행 수량 계산 - - 로또 번호 생성 - - 로또 발행 결과 출력 -2. 당첨 번호 및 보너스 번호 입력 - - 당첨 번호 입력 - - 보너스 번호 입력 -3. 당첨 확인 및 결과 출력 - - 로또 당첨 확인 - - 당첨 내역 집계 - - 당첨 통계 출력 - - 수익률 계산 및 출력 +1. 입력 기능(InputView) + +- 구입금액 입력받기 + - 숫자 형식 검증 + - 양수 검증 + - 1000원 단위 검증 +- 당첨 번호 입력받기 + - 쉼표로 구분된 6개 숫자 입력 + - 1~45범위 검증 + - 중복 번호 검증 +- 보너스 번호 입력받기 + - 숫자 형식 검증 + - 1~45범위 검증 + +2. 로또 생성기능 (LottoMake, Lotto) + +- 로또 번호 자동 생성 + - 1~45 범위에서 중복 없이 6개 숫자 추출 + - 오름차순 정렬 +- 로또 객체 생성 및 검증 + - 로또 번호 개수 검증 + - 중복 번호 검증 + - 번호 범위 검증 +- 구입 금액에 따른 로또 발행 개수 계산 + - 1,000원당 1개 발행 + +3. 당첨 확인 기능 (WinningResult) + +- 각 로또의 당첨 여부 확인 + - 당첨 번호와 일치하는 개수 계산 + - 보너스 번호 포함 여부 확인 +- 당첨 등수 판정 +- 당첨 통계 집계 +- 총 수익금 계산 +- 수익률 계산 + +4. 출력 기능 (OutputView) + +- 구매한 로또 개수 출력 +- 발행된 로또 번호 출력 +- 당첨 통계 출력 + - 각 등수별 당첨 개수 + - 각 등수별 당첨 금액 +- 총 수익률 출력 From 77190c240b43766c5826987ad7a65f6c11b0988f Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 23:32:16 +0900 Subject: [PATCH 15/18] =?UTF-8?q?test:=20Lotto=20test=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..2aa1fe496 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -7,6 +7,12 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); + test("로또 번호가 6개 보다 작으면 예외가 발생한다", () => { + expect(() => { + new Lotto([1, 2, 3]); + }).toThrow("[ERROR]"); + }); + // TODO: 테스트가 통과하도록 프로덕션 코드 구현 test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { expect(() => { From f639d9fd057ec1b3926cb7e5142c8fab5e08f125 Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 23:32:37 +0900 Subject: [PATCH 16/18] =?UTF-8?q?test:=20LottoMaker=20=20test=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoMaker.test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 __tests__/LottoMaker.test.js diff --git a/__tests__/LottoMaker.test.js b/__tests__/LottoMaker.test.js new file mode 100644 index 000000000..fec556ed9 --- /dev/null +++ b/__tests__/LottoMaker.test.js @@ -0,0 +1,30 @@ +import LottoMaker from "../src/LottoMaker"; + +describe("LottoMaker 클래스 테스트", () => { + test("생성된 로또 번호는 1~45 범위 내의 숫자다", () => { + const lotto = LottoMaker.createLottoNumbers(); + const numbers = lotto.getNumbers(); + + numbers.forEach((num) => { + expect(num).toBeGreaterThanOrEqual(1); + expect(num).toBeLessThanOrEqual(45); + }); + }); + + test("생성된 로또 번호는 중복이 없다", () => { + const lotto = LottoMaker.createLottoNumbers(); + const numbers = lotto.getNumbers(); + const uniqueNumbers = new Set(numbers); + + expect(uniqueNumbers.size).toBe(6); + }); + + test("생성된 로또 번호는 오름차순으로 정렬되어 있다", () => { + const lotto = LottoMaker.createLottoNumbers(); + const numbers = lotto.getNumbers(); + + for (let i = 0; i < numbers.length - 1; i++) { + expect(numbers[i]).toBeLessThan(numbers[i + 1]); + } + }); +}); From 0acdf89404093a77d42304eb52fe6e05365fe0ee Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 23:33:10 +0900 Subject: [PATCH 17/18] =?UTF-8?q?test:=20WinningResult=20test=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/WinningResult.test.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 __tests__/WinningResult.test.js diff --git a/__tests__/WinningResult.test.js b/__tests__/WinningResult.test.js new file mode 100644 index 000000000..8e6b781c6 --- /dev/null +++ b/__tests__/WinningResult.test.js @@ -0,0 +1,29 @@ +import Lotto from "../src/Lotto"; +import WinningResult from "../src/WinningResult"; + +describe("Winning Result 클래스 테스트", () => { + test("3개 일치 시 통계가 정확히 동작한다.", () => { + const result = new WinningResult(); + const lotto = new Lotto([1, 2, 3, 10, 11, 12]); + const winningNumbers = [1, 2, 3, 4, 5, 6]; + const bonusNumber = 7; + + result.checkWinning(lotto, winningNumbers, bonusNumber); + const statistics = result.getStatistics(); + expect(statistics["3개 일치"]).toBe(1); + expect(statistics["4개 일치"]).toBe(0); + }); + + test("총 상금을 정확히 계산한다.", () => { + const result = new WinningResult(); + const lotto1 = new Lotto([1, 2, 3, 10, 11, 12]); + const lotto2 = new Lotto([1, 2, 3, 4, 11, 12]); + const winningNumbers = [1, 2, 3, 4, 5, 6]; + const bonusNumber = 7; + result.checkWinning(lotto1, winningNumbers, bonusNumber); + result.checkWinning(lotto2, winningNumbers, bonusNumber); + + const totalPrize = result.calculateTotalPrize(); + expect(totalPrize).toBe(5000 + 50000); + }); +}); From 73f1f8c81d2a51037cb5a832479e37d5d1c63b5b Mon Sep 17 00:00:00 2001 From: dozin Date: Mon, 3 Nov 2025 23:33:35 +0900 Subject: [PATCH 18/18] =?UTF-8?q?test:=20InputView=20test=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/InputView.test.js | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 __tests__/InputView.test.js diff --git a/__tests__/InputView.test.js b/__tests__/InputView.test.js new file mode 100644 index 000000000..9837e746c --- /dev/null +++ b/__tests__/InputView.test.js @@ -0,0 +1,48 @@ +import { Console } from "@woowacourse/mission-utils"; +import InputView from "../src/InputView"; + +jest.mock("@woowacourse/mission-utils", () => ({ + Console: { + readLineAsync: jest.fn(), + print: jest.fn(), + }, +})); + +describe("InputView 클래스 테스트", () => { + let inputView; + beforeEach(() => { + inputView = new InputView(); + jest.clearAllMocks(); + }); + describe("purchaseAmountInput", () => { + test("양수가 아니면 에러가 발생한다.", async () => { + Console.readLineAsync.mockResolvedValue("-1000"); + await expect(inputView.purchaseAmountInput()).rejects.toThrow("[ERROR] "); + }); + + test("숫자가 아니면 예외가 발생한다", async () => { + Console.readLineAsync.mockResolvedValue("abc"); + await expect(inputView.purchaseAmountInput()).rejects.toThrow("[ERROR]"); + }); + }); + + describe("winningNumbersInput", () => { + test("정상적인 당첨 번호 입력 시 배열을 반환한다", async () => { + Console.readLineAsync.mockResolvedValue("1,2,3,4,5,6"); + const numbers = await inputView.winningNumbersInput(); + expect(numbers).toEqual([1, 2, 3, 4, 5, 6]); + }); + test("6개가 아니면 예외가 발생한다", async () => { + Console.readLineAsync.mockResolvedValue("1,2,3,4,5"); + await expect(inputView.winningNumbersInput()).rejects.toThrow( + "[ERROR] 당첨 번호는 6개여야 합니다." + ); + }); + test("중복된 번호가 있으면 예외가 발생한다", async () => { + Console.readLineAsync.mockResolvedValue("1,2,3,4,5,5"); + await expect(inputView.winningNumbersInput()).rejects.toThrow( + "[ERROR] 당첨 번호는 중복될 수 없습니다." + ); + }); + }); +});