diff --git a/README.md b/README.md index 15bb106..2c3062e 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ # javascript-lotto-precourse + +## 기능 구현 +1. 사용자로부터 로또 구입 금액을 입력한다. + 금액은 1000원 단위로 입력받으며 1000원으로 나누어 떨어지지 않으면 예외처리 +2. 구입 금액에 따른 로또 수량에 맞춰 랜덤으로 로또 번호가 6개 생성된다. + 로또 번호의 범위는 1~45이며, 중복되지 않는다. +3. 당첨 번호를 입력받는다. 번호는 쉼표(,)를 기준으로 구분한다. + 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다. +4. 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 종료한다. + 수익률을 계산하는 PrizeCheck 클래스를 만들어 코드 분리를 시도해본다. + +## 예외 처리 +1. 당첨 번호가 6개가 아닌 경우 +2. 로또 번호의 범위가 벗어난 경우 +3. 로또 번호가 중복된 경우 diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 872380c..9b0e288 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -13,9 +13,9 @@ const mockQuestions = (inputs) => { const mockRandoms = (numbers) => { MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); - numbers.reduce((acc, number) => { - return acc.mockReturnValueOnce(number); - }, MissionUtils.Random.pickUniqueNumbersInRange); + numbers.forEach((number) => { + MissionUtils.Random.pickUniqueNumbersInRange.mockReturnValueOnce(number); + }); }; const getLogSpy = () => { @@ -51,7 +51,7 @@ describe("로또 테스트", () => { // given const logSpy = getLogSpy(); - mockRandoms([ + const expectedLottos = [ [8, 21, 23, 41, 42, 43], [3, 5, 11, 16, 32, 38], [7, 11, 16, 35, 36, 44], @@ -60,7 +60,9 @@ describe("로또 테스트", () => { [7, 11, 30, 40, 42, 43], [2, 13, 22, 32, 38, 45], [1, 3, 5, 14, 22, 45], - ]); + ]; + + mockRandoms(expectedLottos); mockQuestions(["8000", "1,2,3,4,5,6", "7"]); // when @@ -91,7 +93,19 @@ describe("로또 테스트", () => { }); }); - test("예외 테스트", async () => { + test("예외 테스트 - 잘못된 입력", async () => { await runException("1000j"); }); + + test("예외 테스트 - 1000원 단위로 입력하지 않음", async () => { + await runException("2000"); + }); + + test("예외 테스트 - 당첨 번호 잘못된 형식", async () => { + await runException("1,2,3,4,5,6,7"); + }); + + test("예외 테스트 - 보너스 번호 잘못된 형식", async () => { + await runException("46"); + }); }); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf6..cdfefca 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -7,12 +7,36 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 + test("로또 번호의 개수가 6개 미만이면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5]); + }).toThrow("[ERROR]"); + }); + test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); }).toThrow("[ERROR]"); }); - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + test("로또 번호가 1부터 45 사이의 숫자가 아니면 예외가 발생한다.", () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + }); + + test("정상적인 로또 번호가 입력되면 예외가 발생하지 않는다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6]); + }).not.toThrow(); + }); + + test("로또 번호가 오름차순으로 정렬된다.", () => { + const lotto = new Lotto([6, 5, 4, 3, 2, 1]); + expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5, 6]); + }); }); diff --git a/src/App.js b/src/App.js index 091aa0a..013e281 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,80 @@ +import { Console } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; +import PrizeChecker from "./PrizeChecker.js"; + class App { - async run() {} + async run() { + try { + const purchaseAmount = await this.getPurchaseAmount(); + const lottoCount = purchaseAmount / 1000; + const lottos = this.generateLottos(lottoCount); + + this.printLottoPurchaseDetails(lottoCount, lottos); + + const winningNumbers = await this.getWinningNumbers(); + const bonusNumber = await this.getBonusNumber(); + + this.printResultsAndRevenueRate(purchaseAmount, lottos, winningNumbers, bonusNumber); + } catch (error) { + Console.print(error.message); + } + } + + async getUserInput(message) { + const input = await Console.readLineAsync(message); + return input.trim(); + } + + async getPurchaseAmount() { + const input = await this.getUserInput(`구입 금액을 입력해주세요.\n`); + const amount = parseInt(input); + this.validatePurchaseAmount(amount); + return amount; + } + + validatePurchaseAmount(amount) { + if (isNaN(amount) || amount % 1000 !== 0) { + throw new Error("[ERROR] 천 원 단위로 입력해야 합니다."); + } + } + + generateLottos(lottoCount) { + return Array.from({ length: lottoCount }, () => Lotto.generateRandomLotto()); + } + + async getWinningNumbers() { + const input = await this.getUserInput("당첨 번호를 입력해주세요. 번호는 쉼표(,)로 구분됩니다.\n"); + const numbers = input.split(",").map(num => parseInt(num.trim())); + const winningLotto = Lotto.validateWinningNumbers(numbers); + return winningLotto.getNumbers(); + } + + async getBonusNumber() { + const input = await this.getUserInput("보너스 번호를 입력해주세요.\n"); + const bonusNumber = parseInt(input); + Lotto.validateBonusNumber(bonusNumber); + return bonusNumber; + } + + printLottoPurchaseDetails(lottoCount, lottos) { + Console.print(`${lottoCount}개를 구매했습니다.`); + lottos.forEach(lotto => Console.print(`[${lotto.getNumbers().join(", ")}]`)); + } + + printResultsAndRevenueRate(purchaseAmount, lottos, winningNumbers, bonusNumber) { + const result = PrizeChecker.checkResults(lottos, winningNumbers, bonusNumber); + this.printResultDetails(result); + const revenueRate = PrizeChecker.calculateRevenueRate(purchaseAmount, result); + Console.print(`총 수익률은 ${revenueRate}%입니다.`); + } + + printResultDetails(result) { + Console.print(`3개 일치 (5,000원) - ${result['3개 일치']}개`); + Console.print(`4개 일치 (50,000원) - ${result['4개 일치']}개`); + Console.print(`5개 일치 (1,500,000원) - ${result['5개 일치']}개`); + Console.print(`5개 일치, 보너스 볼 일치 (30,000,000원) - ${result['5개 일치 (보너스 볼 일치)']}개`); + Console.print(`6개 일치 (2,000,000,000원) - ${result['6개 일치']}개`); + } } -export default App; +export default App; \ No newline at end of file diff --git a/src/Lotto.js b/src/Lotto.js index cb0b152..97d796f 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,3 +1,5 @@ +import { Random } from "@woowacourse/mission-utils"; + class Lotto { #numbers; @@ -10,9 +12,43 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } + this.#checkNumberRange(numbers); + this.#checkDuplicates(numbers); + } + + #checkNumberRange(numbers) { + if (!numbers.every(num => num >= 1 && num <= 45)) { + throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } + + #checkDuplicates(numbers) { + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== 6) { + throw new Error("[ERROR] 로또 번호는 중복되지 않아야 합니다."); + } + } + + static generateRandomLotto() { + return new Lotto(Random.pickUniqueNumbersInRange(1, 45, 6)); } - // TODO: 추가 기능 구현 + getNumbers() { + return [...this.#numbers].sort((a, b) => a - b); + } + + static validateWinningNumbers(numbers) { + if (numbers.length !== 6) { + throw new Error("[ERROR] 당첨 번호는 6개여야 합니다."); + } + return new Lotto(numbers); + } + + static validateBonusNumber(number) { + if (number < 1 || number > 45 || isNaN(number)) { + throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } } export default Lotto; diff --git a/src/PrizeChecker.js b/src/PrizeChecker.js new file mode 100644 index 0000000..f3995e0 --- /dev/null +++ b/src/PrizeChecker.js @@ -0,0 +1,52 @@ +//수익률 계산 +class PrizeChecker { + static checkResults(lottos, winningNumbers, bonusNumber) { + const result = { + '6개 일치': 0, + '5개 일치 (보너스 볼 일치)': 0, + '5개 일치': 0, + '4개 일치': 0, + '3개 일치': 0 + }; + + lottos.forEach(lotto => { + const matchingNumbers = this.getMatchingCount(lotto.getNumbers(), winningNumbers); + this.updateResult(result, matchingNumbers, lotto.getNumbers(), bonusNumber); + }); + + return result; + } + + static getMatchingCount(lottoNumbers, winningNumbers) { + return lottoNumbers.filter(number => winningNumbers.includes(number)).length; + } + + static updateResult(result, matchingNumbers, lottoNumbers, bonusNumber) { + if (matchingNumbers === 6) result['6개 일치']++; + if (matchingNumbers === 5 && lottoNumbers.includes(bonusNumber)) { + result['5개 일치 (보너스 볼 일치)']++; + } + if (matchingNumbers === 5) result['5개 일치']++; + if (matchingNumbers === 4) result['4개 일치']++; + if (matchingNumbers === 3) result['3개 일치']++; + } + + static calculateRevenueRate(purchaseAmount, result) { + const totalPrizeMoney = this.calculateTotalPrizeMoney(result); + return ((totalPrizeMoney / purchaseAmount) * 100).toFixed(1); + } + + static calculateTotalPrizeMoney(result) { + const prizeMoney = { + '3개 일치': 5000, + '4개 일치': 50000, + '5개 일치': 1500000, + '5개 일치 (보너스 볼 일치)': 30000000, + '6개 일치': 2000000000 + }; + + return Object.entries(result).reduce((total, [key, value]) => total + value * prizeMoney[key], 0); + } + } + + export default PrizeChecker; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 02a1d38..100dd7d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ import App from "./App.js"; const app = new App(); -await app.run(); +await app.run(); \ No newline at end of file