From fce079cc0c65077e8e44691cb698bef3f32b63ee Mon Sep 17 00:00:00 2001 From: JuYeong17 Date: Mon, 4 Nov 2024 23:44:27 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 34 +++++++++++ src/App.js | 7 ++- src/Lotto.js | 12 +++- src/controller/Controller.js | 115 +++++++++++++++++++++++++++++++++++ src/model/BuyLotto.js | 8 +++ src/model/LottoRandom.js | 13 ++++ src/model/WinningLotto.js | 10 +++ src/model/WinningResult.js | 20 ++++++ src/view/InputView.js | 17 ++++++ src/view/OutputView.js | 35 +++++++++++ 10 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 src/controller/Controller.js create mode 100644 src/model/BuyLotto.js create mode 100644 src/model/LottoRandom.js create mode 100644 src/model/WinningLotto.js create mode 100644 src/model/WinningResult.js create mode 100644 src/view/InputView.js create mode 100644 src/view/OutputView.js diff --git a/README.md b/README.md index 15bb106b5..293282e83 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..bf975bcd8 --- /dev/null +++ b/src/controller/Controller.js @@ -0,0 +1,115 @@ +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 = this.winningResult.result( + winningNumbers, + this.lottos, + this.bonusNumber + ); + 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..a59da59d0 --- /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..f64561d86 --- /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..a0033445d --- /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; // 2등: 5개 + 보너스 번호 일치 + } 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; From 029ed04e477cdb4446b87e01c664af4de87d6f18 Mon Sep 17 00:00:00 2001 From: JuYeong17 Date: Mon, 4 Nov 2024 23:47:07 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20controller=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/Controller.js | 22 ++++++++++++++++------ src/model/LottoRandom.js | 2 +- src/model/WinningLotto.js | 2 +- src/model/WinningResult.js | 2 +- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/controller/Controller.js b/src/controller/Controller.js index bf975bcd8..0f4d800f5 100644 --- a/src/controller/Controller.js +++ b/src/controller/Controller.js @@ -74,11 +74,21 @@ class LottoController { } async calculateResults(winningNumbers) { - const countMap = this.winningResult.result( - winningNumbers, - this.lottos, - this.bonusNumber - ); + 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, @@ -89,7 +99,7 @@ class LottoController { calculateRateOfReturn(totalPrize, purchaseAmount) { const rate = (totalPrize / purchaseAmount) * 100; - return parseFloat(rate.toFixed(2)); // 소수점 둘째 자리까지 반올림 + return parseFloat(rate.toFixed(2)); } calculateTotalPrize(countMap) { diff --git a/src/model/LottoRandom.js b/src/model/LottoRandom.js index a59da59d0..08efd5df0 100644 --- a/src/model/LottoRandom.js +++ b/src/model/LottoRandom.js @@ -4,7 +4,7 @@ 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 numbers.sort((a, b) => a - b); }); return lottoNumbers; } diff --git a/src/model/WinningLotto.js b/src/model/WinningLotto.js index f64561d86..956abcc11 100644 --- a/src/model/WinningLotto.js +++ b/src/model/WinningLotto.js @@ -2,7 +2,7 @@ class WinningLotto { async winningnumbers(winningNumbersString) { const winningNumbers = winningNumbersString .split(",") - .map((num) => Number(num.trim())); // 문자열을 숫자 배열로 변환 + .map((num) => Number(num.trim())); return winningNumbers; } } diff --git a/src/model/WinningResult.js b/src/model/WinningResult.js index a0033445d..00e4a883e 100644 --- a/src/model/WinningResult.js +++ b/src/model/WinningResult.js @@ -7,7 +7,7 @@ class WinningResult { winningNumbers.includes(num) ).length; if (matchCount === 5 && lotto.includes(bonusNumber)) { - countMap["5+bonus"] += 1; // 2등: 5개 + 보너스 번호 일치 + countMap["5+bonus"] += 1; } else if (countMap[matchCount] !== undefined) { countMap[matchCount] += 1; } From 308793f7c051b7f24615b3374328db8afc46a7c5 Mon Sep 17 00:00:00 2001 From: JuYeong17 Date: Mon, 4 Nov 2024 23:48:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 293282e83..0a5871271 100644 --- a/README.md +++ b/README.md @@ -2,34 +2,34 @@ ### 랜덤 -[x] 숫자의 범위 1~45 중 랜덤 +- [x] 숫자의 범위 1~45 중 랜덤 ### 입력 -[x] 로또 구입 금액 +- [x] 로또 구입 금액 -[x] 1000원 단위로 안떨어지면 예외 +- [x] 1000원 단위로 안떨어지면 예외 -[x] 당첨 번호 입력 쉼표(,)를 기준으로 +- [x] 당첨 번호 입력 쉼표(,)를 기준으로 -[x] 보너스 번호 입력 +- [x] 보너스 번호 입력 ### 출력 -[x] 발행한 로또 수 만큼 번호 출력 +- [x] 발행한 로또 수 만큼 번호 출력 -[x] 로또 번호 오름 차순 +- [x] 로또 번호 오름 차순 -[x] 당첨 내역 출력 +- [x] 당첨 내역 출력 -[x] 수익률( 소수점 둘째 자리 반올림) +- [x] 수익률( 소수점 둘째 자리 반올림) ### 예외 -[x]1000원 단위로 안떨어질때 +- [x]1000원 단위로 안떨어질때 -[x] 로또 입력 번호가 1~45가 아닐시 +- [x] 로또 입력 번호가 1~45가 아닐시 -[x] 보너스 번호 하나 이상일시 +- [x] 보너스 번호 하나 이상일시 -[x] 로또 번호에 문자 입력 시 +- [x] 로또 번호에 문자 입력 시