diff --git a/README.md b/README.md index 15bb106b5..fdf702f01 100644 --- a/README.md +++ b/README.md @@ -1 +1,138 @@ # javascript-lotto-precourse +로또 구매부터 당첨 결과 확인까지 전 과정을 시뮬레이션하는 JavaScript 애플리케이션입니다. + +## 기능 소개 + +1. **로또 구매**: 1,000원 단위로 로또를 구매합니다. +2. **자동 번호 생성**: 1~45 사이의 중복되지 않는 6개의 번호를 자동으로 생성합니다. +3. **당첨 확인**: 구매한 로또와 당첨 번호를 비교하여 당첨 결과를 확인합니다. +4. **수익률 계산**: 총 수익률을 계산하여 표시합니다. + +## 실행 예시 + +``` +구입금액을 입력해 주세요. +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%입니다. +``` + +## 프로젝트 구조 + +``` +src/ +├── App.js # 애플리케이션 진입점 +├── index.js # 프로그램 실행 +├── controller/ +│ └── LottoController.js # 로또 게임 로직 제어 +├── model/ +│ ├── Lotto.js # 로또 클래스 +│ ├── lottoGenerator.js # 로또 생성 로직 +│ ├── calculateResult.js # 당첨 결과 계산 +│ ├── calculateProfit.js # 수익률 계산 +│ ├── validator.js # 입력 검증 +│ └── constants.js # 상수 정의 +└── view/ + ├── inputView.js # 사용자 입력 처리 + └── outputView.js # 결과 출력 처리 + +__tests__/ +├── ApplicationTest.js # 통합 테스트 +├── LottoTest.js # 로또 클래스 테스트 +├── validator.test.js # 유효성 검증 테스트 +├── LottoGenerator.test.js # 로또 생성 테스트 +├── calculateResult.test.js # 결과 계산 테스트 +└── calculateProfit.test.js # 수익률 계산 테스트 +``` + +## 구현 기능 목록 + +### 입력 +- [x] 로또 구입 금액 입력 + - [x] 숫자가 아닌 경우 예외 + - [x] 정수가 아닌 경우 예외 + - [x] 양수가 아닌 경우 예외 + - [x] `1000`으로 나누어 떨어지지 않는 경우 예외 +- [x] 당첨 번호 입력 + - [x] 쉼표로 구분된 6개의 숫자 입력 + - [x] `1`~`45` 범위를 벗어나는 경우 예외 + - [x] 중복된 번호가 있는 경우 예외 +- [x] 보너스 번호 입력 + - [x] `1`~`45` 범위를 벗어나는 경우 예외 + - [x] 당첨 번호와 중복인 경우 예외 + +### 로또 발행 +- [x] `구입 금액 / 1000`개의 로또 발행 +- [x] 각 로또는 `1`~`45` 범위의 중복되지 않은 6개의 랜덤 숫자로 구성 +- [x] 번호 오름차순 정렬 + +### 당첨 결과 계산 +- [x] 로또 번호에 따른 당첨 결과 계산 + - 1등: 6개 번호 일치 / 2,000,000,000원 + - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 + - 3등: 5개 번호 일치 / 1,500,000원 + - 4등: 4개 번호 일치 / 50,000원 + - 5등: 3개 번호 일치 / 5,000원 +- [x] 수익률 계산 + - [x] 소수점 둘째 자리 반올림 + +### 출력 +- [x] 구매한 로또 번호 출력 +- [x] 당첨 통계 출력 +- [x] 수익률 출력 +- [x] 예외 발생 시 `[ERROR]`로 시작하는 에러 메시지 출력 + +## 예외 처리 + +모든 예외 상황에서 `[ERROR]`로 시작하는 에러 메시지를 출력하고, 올바른 값을 재입력받습니다. + +### 구입 금액 예외 +- 숫자가 아닌 값 입력 시: `[ERROR] 구입 금액은 숫자여야 합니다.` +- 정수가 아닌 값 입력 시: `[ERROR] 구입 금액은 정수여야 합니다.` +- 0 이하의 값 입력 시: `[ERROR] 구입 금액은 0보다 커야 합니다.` +- 1,000원 단위가 아닌 경우: `[ERROR] 구입 금액은 1000원 단위여야 합니다.` + +### 당첨 번호 예외 +- 6개가 아닌 경우: `[ERROR] 당첨 번호는 6개여야 합니다.` +- 숫자가 아닌 값 포함 시: `[ERROR] 당첨 번호는 숫자여야 합니다.` +- 1~45 범위를 벗어난 경우: `[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.` +- 중복된 번호가 있는 경우: `[ERROR] 중복된 당첨 번호가 있습니다.` + +### 보너스 번호 예외 +- 1~45 범위를 벗어난 경우: `[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.` +- 당첨 번호와 중복인 경우: `[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.` + +### 로또 클래스 예외 +- 번호가 6개가 아닌 경우: `[ERROR] 로또 번호는 6개여야 합니다.` +- 중복된 번호가 있는 경우: `[ERROR] 로또 번호에 중복된 숫자가 있습니다.` + +## 설계 시 고려한 부분 + +1. **MVC 패턴**: Controller, Model, View를 분리하여 관심사의 분리 원칙을 준수합니다. +2. **단일 책임 원칙**: 각 모듈은 하나의 책임만 가집니다. +3. **입력 검증**: 모든 사용자 입력은 validator를 통해 검증됩니다. +4. **에러 처리**: 예외 발생 시 적절한 에러 메시지를 표시합니다. +5. **테스트**: 단위 테스트와 통합 테스트를 통해 코드의 안정성을 보장합니다. \ No newline at end of file diff --git a/__tests__/LottoGenerator.test.js b/__tests__/LottoGenerator.test.js new file mode 100644 index 000000000..71ec1c8d1 --- /dev/null +++ b/__tests__/LottoGenerator.test.js @@ -0,0 +1,38 @@ +import { Random } from "@woowacourse/mission-utils"; +import { generateLottos } from "../src/model/lottoGenerator.js"; +import Lotto from "../src/model/Lotto.js"; + +jest.mock("@woowacourse/mission-utils", () => ({ + Random: { + pickUniqueNumbersInRange: jest.fn(), + }, +})); + +describe("generateLottos()", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("구입 금액에 따라 Lotto 인스턴스를 생성한다.", () => { + Random.pickUniqueNumbersInRange.mockReturnValue([1, 2, 3, 4, 5, 6]); + const purchaseAmount = 3000; + + const lottos = generateLottos(purchaseAmount); + + expect(lottos).toHaveLength(3); + lottos.forEach((lotto) => { + expect(lotto).toBeInstanceOf(Lotto); + }); + }); + + test("각 로또는 1~45 범위의 중복되지 않은 6개의 오름차순 숫자로 구성된다.", () => { + Random.pickUniqueNumbersInRange.mockReturnValue([45, 1, 12, 8, 30, 5]); + + const lottos = generateLottos(1000); + const numbers = lottos[0].getNumbers ? lottos[0].getNumbers() : undefined; + + if (numbers) { + expect(numbers).toEqual([1, 5, 8, 12, 30, 45]); + } + }); +}); \ No newline at end of file diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..b6dd22eb8 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,4 +1,4 @@ -import Lotto from "../src/Lotto"; +import Lotto from "../src/model/Lotto"; describe("로또 클래스 테스트", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { @@ -7,12 +7,9 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); }).toThrow("[ERROR]"); }); - - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 }); diff --git a/__tests__/calculateProfit.test.js b/__tests__/calculateProfit.test.js new file mode 100644 index 000000000..65aed3743 --- /dev/null +++ b/__tests__/calculateProfit.test.js @@ -0,0 +1,54 @@ +import calculateProfit from "../src/model/calculateProfit.js"; + +describe("calculateProfit", () => { + test("당첨 내역이 없으면 수익률은 0%", () => { + const result = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + const purchaseAmount = 10000; + const rate = calculateProfit(result, purchaseAmount); + + expect(rate).toBe(0); + }); + + test("1등 1개면 200,000,000%", () => { + const result = { 1: 1, 2: 0, 3: 0, 4: 0, 5: 0 }; + const purchaseAmount = 1000; + const rate = calculateProfit(result, purchaseAmount); + + const expected = Number(((2000000000 / 1000) * 100).toFixed(1)); + expect(rate).toBe(expected); + }); + + test("3등 2개면 (1,500,000 × 2) / 10,000 * 100 = 30,000%", () => { + const result = { 1: 0, 2: 0, 3: 2, 4: 0, 5: 0 }; + const purchaseAmount = 10000; + const rate = calculateProfit(result, purchaseAmount); + + expect(rate).toBe(30000); + }); + + test("5등 5개면 (5,000 × 5) / 5,000 * 100 = 500%", () => { + const result = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 5 }; + const purchaseAmount = 5000; + const rate = calculateProfit(result, purchaseAmount); + + expect(rate).toBe(500); + }); + + test("1등 1명, 3등 2명, 5등 3명일 때 총 상금은 2,003,015,000원이다.", () => { + const result = { 1: 1, 2: 0, 3: 2, 4: 0, 5: 3 }; + const purchaseAmount = 10000; + const expected = Number(((2003015000 / purchaseAmount) * 100).toFixed(1)); + const rate = calculateProfit(result, purchaseAmount); + + expect(rate).toBe(expected); + }); + + test("결과값은 소수점 한 자리까지 반올림된다.", () => { + const result = { 1: 0, 2: 1, 3: 0, 4: 0, 5: 0 }; + const purchaseAmount = 3333; + const rate = calculateProfit(result, purchaseAmount); + + const expected = Number(((30000000 / 3333) * 100).toFixed(1)); + expect(rate).toBe(expected); + }); +}); \ No newline at end of file diff --git a/__tests__/calculateResult.test.js b/__tests__/calculateResult.test.js new file mode 100644 index 000000000..f4c8ec1ea --- /dev/null +++ b/__tests__/calculateResult.test.js @@ -0,0 +1,63 @@ +import calculateResult from "../src/model/calculateResult.js"; + +const createLotto = (numbers) => ({ + getNumbers: () => numbers, +}); + +describe("calculateResult", () => { + test("6개 번호가 모두 일치하면 1등 1개를 반환한다.", () => { + const lottos = [createLotto([1, 2, 3, 4, 5, 6])]; + const result = calculateResult(lottos, [1, 2, 3, 4, 5, 6], 7); + + expect(result).toEqual({ 1: 1, 2: 0, 3: 0, 4: 0, 5: 0 }); + }); + + test("5개 번호 + 보너스 번호가 일치하면 2등 1개를 반환한다.", () => { + const lottos = [createLotto([1, 2, 3, 4, 5, 7])]; + const result = calculateResult(lottos, [1, 2, 3, 4, 5, 6], 7); + + expect(result).toEqual({ 1: 0, 2: 1, 3: 0, 4: 0, 5: 0 }); + }); + + test("5개 번호가 일치하면 3등 1개를 반환한다.", () => { + const lottos = [createLotto([1, 2, 3, 4, 5, 9])]; + const result = calculateResult(lottos, [1, 2, 3, 4, 5, 6], 7); + + expect(result).toEqual({ 1: 0, 2: 0, 3: 1, 4: 0, 5: 0 }); + }); + + test("4개 번호가 일치하면 4등 1개를 반환한다.", () => { + const lottos = [createLotto([1, 2, 3, 4, 10, 11])]; + const result = calculateResult(lottos, [1, 2, 3, 4, 5, 6], 7); + + expect(result).toEqual({ 1: 0, 2: 0, 3: 0, 4: 1, 5: 0 }); + }); + + test("3개 번호가 일치하면 5등 1개를 반환한다.", () => { + const lottos = [createLotto([1, 2, 3, 10, 11, 12])]; + const result = calculateResult(lottos, [1, 2, 3, 4, 5, 6], 7); + + expect(result).toEqual({ 1: 0, 2: 0, 3: 0, 4: 0, 5: 1 }); + }); + + test("2개 이하로 일치하면 등수에 포함되지 않는다.", () => { + const lottos = [createLotto([1, 2, 10, 11, 12, 13])]; + const result = calculateResult(lottos, [1, 2, 3, 4, 5, 6], 7); + + expect(result).toEqual({ 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }); + }); + + test("여러 장의 로또 결과가 등수별로 누적된다.", () => { + const lottos = [ + createLotto([1, 2, 3, 4, 5, 6]), // 1등 + createLotto([1, 2, 3, 4, 5, 7]), // 2등 + createLotto([1, 2, 3, 4, 5, 9]), // 3등 + createLotto([1, 2, 3, 4, 10, 11]), // 4등 + createLotto([1, 2, 3, 10, 11, 12]), // 5등 + ]; + + const result = calculateResult(lottos, [1, 2, 3, 4, 5, 6], 7); + + expect(result).toEqual({ 1: 1, 2: 1, 3: 1, 4: 1, 5: 1 }); + }); +}); \ No newline at end of file diff --git a/__tests__/validator.test.js b/__tests__/validator.test.js new file mode 100644 index 000000000..985a13313 --- /dev/null +++ b/__tests__/validator.test.js @@ -0,0 +1,85 @@ +import validator from "../src/model/validator.js"; + +describe("Validator.validatePurchaseAmount()", () => { + test("정상 입력(1000원 단위의 양의 정수)일 경우 에러가 발생하지 않는다.", () => { + expect(() => validator.validatePurchaseAmount(1000)).not.toThrow(); + expect(() => validator.validatePurchaseAmount(8000)).not.toThrow(); + }); + + test("숫자가 아닐 경우 에러가 발생한다.", () => { + expect(() => validator.validatePurchaseAmount(NaN)).toThrow("[ERROR] 구입 금액은 숫자여야 합니다."); + }); + + test("정수가 아닐 경우 에러가 발생한다.", () => { + expect(() => validator.validatePurchaseAmount(1000.5)).toThrow("[ERROR] 구입 금액은 정수여야 합니다."); + }); + + test("0 이하일 경우 에러가 발생한다.", () => { + expect(() => validator.validatePurchaseAmount(0)).toThrow("[ERROR] 구입 금액은 0보다 커야 합니다."); + expect(() => validator.validatePurchaseAmount(-5000)).toThrow("[ERROR] 구입 금액은 0보다 커야 합니다."); + }); + + test("1000원 단위가 아닐 경우 에러가 발생한다.", () => { + expect(() => validator.validatePurchaseAmount(1500)).toThrow("[ERROR] 구입 금액은 1000원 단위여야 합니다."); + expect(() => validator.validatePurchaseAmount(123456)).toThrow("[ERROR] 구입 금액은 1000원 단위여야 합니다."); + }); +}); + +describe("Validator.validateWinningNumbers()", () => { + test("정상 입력일 경우 에러가 발생하지 않는다.", () => { + expect(() => validator.validateWinningNumbers([1, 5, 12, 23, 34, 45])).not.toThrow(); + }); + + test("번호가 6개가 아닐 경우 에러가 발생한다.", () => { + expect(() => validator.validateWinningNumbers([1, 2, 3, 4, 5])).toThrow("[ERROR] 당첨 번호는 6개여야 합니다."); + expect(() => validator.validateWinningNumbers([1, 2, 3, 4, 5, 6, 7])).toThrow("[ERROR] 당첨 번호는 6개여야 합니다."); + }); + + test("숫자가 아닌 값이 있을 경우 에러가 발생한다.", () => { + expect(() => validator.validateWinningNumbers([1, 2, "3", 4, 5, 6])).toThrow("[ERROR] 당첨 번호는 숫자여야 합니다."); + expect(() => validator.validateWinningNumbers([1, 2, null, 4, 5, 6])).toThrow("[ERROR] 당첨 번호는 숫자여야 합니다."); + }); + + test("1 미만의 번호가 있을 경우 에러가 발생한다.", () => { + expect(() => validator.validateWinningNumbers([0, 2, 3, 4, 5, 6])).toThrow("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + expect(() => validator.validateWinningNumbers([-1, 2, 3, 4, 5, 6])).toThrow("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + }); + + test("45 초과의 번호가 있을 경우 에러가 발생한다.", () => { + expect(() => validator.validateWinningNumbers([1, 2, 3, 4, 5, 46])).toThrow("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + }); + + test("중복된 번호가 있을 경우 에러가 발생한다.", () => { + expect(() => validator.validateWinningNumbers([1, 2, 3, 3, 4, 5])).toThrow("[ERROR] 중복된 당첨 번호가 있습니다."); + }); +}); + +describe("Validator.validateBonusNumber()", () => { + test("정상 입력일 경우 에러가 발생하지 않는다.", () => { + const winningNumbers = [1, 2, 3, 4, 5, 6]; + expect(() => validator.validateBonusNumber(7, winningNumbers)).not.toThrow(); + expect(() => validator.validateBonusNumber(45, winningNumbers)).not.toThrow(); + }); + + test("보너스 번호가 1 미만일 경우 에러가 발생한다.", () => { + const winningNumbers = [10, 20, 30, 40, 41, 42]; + expect(() => validator.validateBonusNumber(0, winningNumbers)) + .toThrow("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + expect(() => validator.validateBonusNumber(-5, winningNumbers)) + .toThrow("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + }); + + test("보너스 번호가 45 초과일 경우 에러가 발생한다.", () => { + const winningNumbers = [1, 2, 3, 4, 5, 6]; + expect(() => validator.validateBonusNumber(46, winningNumbers)) + .toThrow("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + expect(() => validator.validateBonusNumber(100, winningNumbers)) + .toThrow("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + }); + + test("보너스 번호가 당첨 번호와 중복될 경우 에러가 발생한다.", () => { + const winningNumbers = [7, 8, 9, 10, 11, 12]; + expect(() => validator.validateBonusNumber(9, winningNumbers)) + .toThrow("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + }); +}); \ No newline at end of file diff --git a/src/App.js b/src/App.js index 091aa0a5d..c121a4315 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.run(); + } } export default App; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 000000000..4d928a279 --- /dev/null +++ b/src/controller/LottoController.js @@ -0,0 +1,61 @@ +import { Console } from "@woowacourse/mission-utils"; +import inputView from "../view/inputView.js"; +import outputView from "../view/outputView.js"; +import validator from "../model/validator.js"; +import generateLottos from "../model/lottoGenerator.js"; +import calculateResult from "../model/calculateResult.js"; +import calculateProfit from "../model/calculateProfit.js"; + +class LottoController { + async run() { + const purchaseAmount = await this.getPurchaseAmount(); + const lottos = generateLottos(purchaseAmount); + outputView.printPurchasedLottos(lottos); + + const winningNumbers = await this.getWinningNumbers(); + const bonusNumber = await this.getBonusNumber(winningNumbers); + + const result = calculateResult(lottos, winningNumbers, bonusNumber); + const profitRate = calculateProfit(result, purchaseAmount); + + outputView.printResult(result, profitRate); + } + + async getPurchaseAmount() { + try { + const input = await inputView.readPurchaseAmount(); + const purchaseAmount = Number(input); + validator.validatePurchaseAmount(purchaseAmount); + return purchaseAmount; + } catch (error) { + Console.print(error.message); + return this.getPurchaseAmount(); + } + } + + async getWinningNumbers() { + try { + const input = await inputView.readWinningNumbers(); + const winningNumbers = input.split(",").map((num) => Number(num.trim())); + validator.validateWinningNumbers(winningNumbers); + return winningNumbers; + } catch (error) { + Console.print(error.message); + return this.getWinningNumbers(); + } + } + + async getBonusNumber(winningNumbers) { + try { + const input = await inputView.readBonusNumber(); + const bonusNumber = Number(input); + validator.validateBonusNumber(bonusNumber, winningNumbers); + return bonusNumber; + } catch (error) { + Console.print(error.message); + return this.getBonusNumber(winningNumbers); + } + } +} + +export default LottoController; \ No newline at end of file diff --git a/src/Lotto.js b/src/model/Lotto.js similarity index 54% rename from src/Lotto.js rename to src/model/Lotto.js index cb0b1527e..fdbc6353f 100644 --- a/src/Lotto.js +++ b/src/model/Lotto.js @@ -10,9 +10,16 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } + + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== numbers.length) { + throw new Error("[ERROR] 로또 번호에 중복된 숫자가 있습니다."); + } } - // TODO: 추가 기능 구현 + getNumbers() { + return this.#numbers; + } } export default Lotto; diff --git a/src/model/calculateProfit.js b/src/model/calculateProfit.js new file mode 100644 index 000000000..72de38f3d --- /dev/null +++ b/src/model/calculateProfit.js @@ -0,0 +1,12 @@ +import { PRIZE_TABLE } from "./constants.js"; + +export default function calculateProfit(result, purchaseAmount) { + let totalPrize = 0; + Object.keys(result).forEach((rank) => { + const count = result[rank]; + const prize = PRIZE_TABLE.find((p) => p.rank === Number(rank)); + if (prize) totalPrize += prize.amount * count; + }); + const rate = (totalPrize / purchaseAmount) * 100; + return Number(rate.toFixed(1)); +} \ No newline at end of file diff --git a/src/model/calculateResult.js b/src/model/calculateResult.js new file mode 100644 index 000000000..000c9203d --- /dev/null +++ b/src/model/calculateResult.js @@ -0,0 +1,30 @@ +import { PRIZE_TABLE } from "./constants.js"; + +export default function calculateResult(lottos, winningNumbers, bonusNumber) { + const result = initResult(); + + lottos.forEach((lotto) => { + const { matchCount, hasBonus } = analyzeLotto(lotto, winningNumbers, bonusNumber); + const prize = findPrize(matchCount, hasBonus); + if (prize) result[prize.rank]++; + }); + + return result; +} + +function initResult() { + return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; +} + +function analyzeLotto(lotto, winningNumbers, bonusNumber) { + const lottoNumbers = lotto.getNumbers(); + const matchCount = lottoNumbers.filter((num) => winningNumbers.includes(num)).length; + const hasBonus = lottoNumbers.includes(bonusNumber); + return { matchCount, hasBonus }; +} + +function findPrize(matchCount, hasBonus) { + return PRIZE_TABLE.find( + (p) => p.match === matchCount && (!!p.bonus === hasBonus) + ); +} \ No newline at end of file diff --git a/src/model/constants.js b/src/model/constants.js new file mode 100644 index 000000000..0f19f3d22 --- /dev/null +++ b/src/model/constants.js @@ -0,0 +1,7 @@ +export const PRIZE_TABLE = [ + { match: 6, bonus: false, rank: 1, amount: 2_000_000_000 }, + { match: 5, bonus: true, rank: 2, amount: 30_000_000 }, + { match: 5, bonus: false, rank: 3, amount: 1_500_000 }, + { match: 4, bonus: false, rank: 4, amount: 50_000 }, + { match: 3, bonus: false, rank: 5, amount: 5_000 }, +]; \ No newline at end of file diff --git a/src/model/lottoGenerator.js b/src/model/lottoGenerator.js new file mode 100644 index 000000000..4609874ac --- /dev/null +++ b/src/model/lottoGenerator.js @@ -0,0 +1,12 @@ +import { Random } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; + +export function generateLottos(purchaseAmount) { + const count = purchaseAmount / 1000; + return Array.from({ length: count }, () => { + const numbers = Random.pickUniqueNumbersInRange(1, 45, 6).sort((a, b) => a - b); + return new Lotto(numbers); + }); +} + +export default generateLottos; \ No newline at end of file diff --git a/src/model/validator.js b/src/model/validator.js new file mode 100644 index 000000000..9c5a07a5d --- /dev/null +++ b/src/model/validator.js @@ -0,0 +1,47 @@ +const validator = { + validatePurchaseAmount(purchaseAmount) { + if (Number.isNaN(purchaseAmount)) { + throw new Error("[ERROR] 구입 금액은 숫자여야 합니다."); + } + if (!Number.isInteger(purchaseAmount)) { + throw new Error("[ERROR] 구입 금액은 정수여야 합니다."); + } + if (purchaseAmount <= 0) { + throw new Error("[ERROR] 구입 금액은 0보다 커야 합니다."); + } + if (purchaseAmount % 1000 !== 0) { + throw new Error("[ERROR] 구입 금액은 1000원 단위여야 합니다."); + } + }, + + validateWinningNumbers(winningNumbers) { + if (winningNumbers.length !== 6) { + throw new Error("[ERROR] 당첨 번호는 6개여야 합니다."); + } + + winningNumbers.forEach((num) => { + if (!Number.isInteger(num)) { + throw new Error("[ERROR] 당첨 번호는 숫자여야 합니다."); + } + if (num < 1 || num > 45) { + throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + }); + + const hasDuplicate = new Set(winningNumbers).size !== winningNumbers.length; + if (hasDuplicate) { + throw new Error("[ERROR] 중복된 당첨 번호가 있습니다."); + } + }, + + validateBonusNumber(bonus, winningNumbers) { + if (bonus < 1 || bonus > 45) { + throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + if (winningNumbers.includes(bonus)) { + throw new Error("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + }, +} + +export default validator; \ No newline at end of file diff --git a/src/view/inputView.js b/src/view/inputView.js new file mode 100644 index 000000000..4b7115df5 --- /dev/null +++ b/src/view/inputView.js @@ -0,0 +1,20 @@ +import { Console } from "@woowacourse/mission-utils"; + +const inputView = { + async readPurchaseAmount() { + const input = await Console.readLineAsync("구입금액을 입력해 주세요.\n"); + return input; + }, + + async readWinningNumbers() { + const input = await Console.readLineAsync("당첨 번호를 입력해 주세요.\n"); + return input; + }, + + async readBonusNumber() { + const input = await Console.readLineAsync("보너스 번호를 입력해 주세요.\n"); + return input; + }, +} + +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..2055c2790 --- /dev/null +++ b/src/view/outputView.js @@ -0,0 +1,36 @@ +import { Console } from "@woowacourse/mission-utils"; +import { PRIZE_TABLE } from "../model/constants.js"; + +const outputView = { + printPurchasedLottos(lottos) { + Console.print(`\n${lottos.length}개를 구매했습니다.`); + lottos.forEach((lotto) => { + Console.print(`[${lotto.getNumbers().join(", ")}]`); + }); + }, + + printResult(result, profitRate) { + Console.print("\n당첨 통계"); + Console.print("----------"); + this.printStatistics(result); + this.printProfitRate(profitRate); + }, + + printStatistics(result) { + PRIZE_TABLE.forEach(({ match, bonus, amount, rank }) => { + let label = `${match}개 일치`; + if (bonus) { + label += ", 보너스 볼 일치"; + } + label += ` (${amount.toLocaleString()}원) - ${result[rank]}개`; + + Console.print(label); + }); + }, + + printProfitRate(profitRate) { + Console.print(`총 수익률은 ${profitRate}%입니다.`); + }, +}; + +export default outputView; \ No newline at end of file