diff --git a/README.md b/README.md index aff2c46597..bb754eb29c 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ class Lotto { #validate(numbers) { if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); } } @@ -237,3 +237,31 @@ class Lotto { - **Git의 커밋 단위는 앞 단계에서 `docs/README.md`에 정리한 기능 목록 단위**로 추가한다. - [커밋 메시지 컨벤션](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 가이드를 참고해 커밋 메시지를 작성한다. - 과제 진행 및 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고한다. + +## 구현 기능 목록 + +### 1. 도메인 로직 + +- 로또번호 생성 기능 + - 지정 범위 내에서 원하는 개수의 숫자를 무작위로 생성하는 기능 + - 무작위로 생성된 숫자가 중복되지 않는 기능 + - 생성된 번호 오름차순 정렬 기능 +- 수익률 계산 및 소수점 둘째 자리 반올림 +- 로또 번호 당첨 및 수익금 통계 + +### 2. UI 담당 + +- 입력 + - 구입금액 + - 당첨 번호 + - 보너스 번호 +- 출력 + - 발행한 로또 수량 및 번호 + - 수익률 및 당첨 내역 + - 에러 문구 `예시) [ERROR] 숫자가 잘못된 형식입니다.` + +### 3. 기타 + +- 에러 처리 + - 구입 금액 입력 시 천원 단위 확인 + - 당첨 번호와 보너스 번호 입력 시, 중복 입력 체크 diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 97bd457659..13494d8468 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,18 +1,23 @@ -import Lotto from "../src/Lotto.js"; +import Lotto from '../src/Model/Lotto'; -describe("로또 클래스 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { +describe('로또 클래스 테스트', () => { + test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); + }).toThrow('[ERROR]'); }); // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { + test('로또 번호에 중복된 숫자가 있으면 예외가 발생한다.', () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); + }).toThrow('[ERROR]'); }); // 아래에 추가 테스트 작성 가능 + test('로또 번호에 1~45가 아닌 숫자가 있으면 예외가 발생한다.', () => { + expect(() => { + new Lotto([1, 2, 3, 46, 57, 58]); + }).toThrow('[ERROR]'); + }); }); diff --git a/src/App.js b/src/App.js index c38b30d5b2..79e6e6ebb2 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,19 @@ +import Controller from './Controller/Controller.js'; + class App { - async play() {} + constructor() { + this.controller = new Controller(); + // this.play(); + } + + async play() { + await this.controller.buyLotto(); + await this.controller.inputWinningNum(); + this.controller.winningStatement(); + } } export default App; + +const app = new App(); +app.play(); diff --git a/src/Controller/Controller.js b/src/Controller/Controller.js new file mode 100644 index 0000000000..ed508e9c9f --- /dev/null +++ b/src/Controller/Controller.js @@ -0,0 +1,49 @@ +import { Console } from '@woowacourse/mission-utils'; +import Input from '../View/Input.js'; +import Output from '../View/Output.js'; +import Lotto from '../Model/Lotto.js'; +import LottoService from '../Model/LottoService.js'; + +class Controller { + #amount; + #lottoList = []; + #winnginNum; + #bonusNum; + #matchHistory; + #rateOfReturn; + + constructor() { + this.input = new Input(); + this.output = new Output(); + this.lottoService = new LottoService(); + } + + async buyLotto() { + this.#amount = await this.input.inputPurchase(); + let buy = this.#amount; + while (buy > 0) { + this.#lottoList.push(this.lottoService.createLotto()); + buy -= 1000; + } + this.output.purchaseHistory(this.#lottoList); + } + + async inputWinningNum() { + const userInput = await this.input.inputWinningNum(); + this.#winnginNum = userInput.split(','); + const lotto = new Lotto(this.#winnginNum); + this.#bonusNum = await this.input.inputBonus(); + } + + winningStatement() { + this.#matchHistory = this.lottoService.getWinningList( + this.#lottoList, + this.#winnginNum, + this.#bonusNum, + ); + this.#rateOfReturn = this.lottoService.getRateReturn(this.#amount); + this.output.winningDetails(this.#matchHistory, this.#rateOfReturn); + } +} + +export default Controller; diff --git a/src/Lotto.js b/src/Lotto.js deleted file mode 100644 index cb0b1527e9..0000000000 --- a/src/Lotto.js +++ /dev/null @@ -1,18 +0,0 @@ -class Lotto { - #numbers; - - constructor(numbers) { - this.#validate(numbers); - this.#numbers = numbers; - } - - #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); - } - } - - // TODO: 추가 기능 구현 -} - -export default Lotto; diff --git a/src/Model/Lotto.js b/src/Model/Lotto.js new file mode 100644 index 0000000000..cfb1690f44 --- /dev/null +++ b/src/Model/Lotto.js @@ -0,0 +1,36 @@ +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#winningNumValid(numbers); + this.#isDuplicate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + if (numbers.length !== 6) { + throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); + } + } + + // TODO: 추가 기능 구현 + #winningNumValid(numbers) { + for (let i = 0; i < numbers.length; i++) { + if (numbers[i] > 45 || numbers[i] < 1) { + throw new Error( + '[ERROR] 로또 번호는 1~45 사이의 숫자만 있어야 합니다.', + ); + } + } + } + + #isDuplicate(numbers) { + const set = new Set(numbers); + if (set.length !== 6) { + throw new Error('[ERROR] 로또 번호에 중복된 숫자가 있으면 안됩니다.'); + } + } +} + +export default Lotto; diff --git a/src/Model/LottoService.js b/src/Model/LottoService.js new file mode 100644 index 0000000000..a26da8b300 --- /dev/null +++ b/src/Model/LottoService.js @@ -0,0 +1,48 @@ +import { Console, Random } from '@woowacourse/mission-utils'; + +class LottoService { + #MIN_NUM = 1; + #MAX_NUM = 45; + #CNT = 6; + #winningList = [0, 0, 0, 0, 0]; + + createLotto() { + const randomList = Random.pickUniqueNumbersInRange( + this.#MIN_NUM, + this.#MAX_NUM, + this.#CNT, + ); + const sortRandomList = randomList.sort((a, b) => { + return a - b; + }); + return sortRandomList; + } + + getRateReturn(purchase) { + let revenue = 0; + const money = [5000, 50000, 1500000, 30000000, 2000000000]; + for (let i = 0; i < this.#winningList.length; i++) { + revenue += money[i] * this.#winningList[i]; + } + return parseFloat((revenue / purchase) * 100).toFixed(1); + } + + getWinningList(userNumList, winningNumList, bonus) { + for (const lotto of userNumList) { + let match = this.matchNums(lotto, winningNumList); + const hasBonus = lotto.includes(bonus - '0'); + if (match === 6 || (match === 5 && hasBonus)) { + match++; + } + this.#winningList[match - 3]++; + } + return this.#winningList; + } + + matchNums(numList, winningNumList) { + const result = numList.filter((num) => winningNumList.includes(`${num}`)); + return result.length; + } +} + +export default LottoService; diff --git a/src/View/Input.js b/src/View/Input.js new file mode 100644 index 0000000000..196599804e --- /dev/null +++ b/src/View/Input.js @@ -0,0 +1,47 @@ +import { Console } from '@woowacourse/mission-utils'; +import { MESSAGES } from './message.js'; + +class Input { + #purchase; + #isValid; + async inputPurchase() { + while (true) { + try { + this.#purchase = await Console.readLineAsync(MESSAGES.PURCHASE_INPUT); + this.#isValid = this.purchaseValid(this.#purchase); + if (this.#isValid) { + return this.#purchase; + } + } catch (e) { + Console.print(e.message); + } + } + } + + purchaseValid(purchase) { + let valid = true; + if (isNaN(purchase)) { + valid = false; + throw new Error('[ERROR] 숫자를 입력해 주세요.'); + } else if (parseInt(purchase) < 1000) { + valid = false; + throw new Error('[ERROR] 1000원 이상 입력해 주세요.'); + } else if (parseInt(purchase) % 1000 !== 0) { + valid = false; + throw new Error('[ERROR] 1000원단위로 입력해주세요'); + } + return valid; + } + + async inputWinningNum() { + const winning = await Console.readLineAsync(MESSAGES.WINNING_INPUT); + return winning; + } + + async inputBonus() { + const bonus = await Console.readLineAsync(MESSAGES.BONUS_INPUT); + return bonus; + } +} + +export default Input; diff --git a/src/View/Output.js b/src/View/Output.js new file mode 100644 index 0000000000..e3b55e9491 --- /dev/null +++ b/src/View/Output.js @@ -0,0 +1,22 @@ +import { Console } from '@woowacourse/mission-utils'; +import { PRIZE_MESSAGES } from './message.js'; + +class Output { + purchaseHistory(purchaseList) { + Console.print(`\n${purchaseList.length}개를 구매했습니다.`); + for (const lotto in purchaseList) { + const msg = purchaseList[lotto].join(', '); + Console.print(`[${msg}]`); + } + } + + winningDetails(matchNum, rateReturn) { + Console.print('\n당첨통계\n---'); + for (let i = 0; i < matchNum.length; i += 1) { + Console.print(`${PRIZE_MESSAGES[i]} - ${matchNum[i]}개`); + } + Console.print(`총 수익률은 ${rateReturn}%입니다.`); + } +} + +export default Output; diff --git a/src/View/message.js b/src/View/message.js new file mode 100644 index 0000000000..4cef58bbab --- /dev/null +++ b/src/View/message.js @@ -0,0 +1,13 @@ +export const MESSAGES = { + PURCHASE_INPUT: '구입금액을 입력해 주세요.\n', + WINNING_INPUT: '\n당첨 번호를 입력해 주세요.\n', + BONUS_INPUT: '\n보너스 번호를 입력해 주세요.\n', +}; + +export const PRIZE_MESSAGES = [ + '3개 일치 (5,000원)', + '4개 일치 (50,000원)', + '5개 일치 (1,500,000원)', + '5개 일치, 보너스 볼 일치 (30,000,000원)', + '6개 일치 (2,000,000,000원)', +];