diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..78036aa3f3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,125 @@ +# 로또 + +> 본 미션은 하단과 같은 흐름을 가지는 로또 구매부터 결과 출력까지의 과정을 프로그래밍 하는 것이다. + +``` +구입금액을 입력해 주세요. +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%입니다. +``` + +## 구현할 기능 목록 순서 + +``` +- 로또 구입을 위한 금액을 입력 받는다. +- 구매한 로또 개수를 출력한다. +- 구매 개수만큼 n개의 유한한 자동 로또를 생성한다. +- 당첨번호 6개를 입력한다. +- 보너스 번호를 입력한다. +- 구매한 로또 번호들과 당첨 번호들을 비교한다. +- 당첨(등수) 결과를 얻는다. +- 당첨 결과와 받은 금액을 통해 수익률을 얻는다. +- 당첨 결과들과 수익률을 출력한다. +``` + +## 역할에 따른 기능 + +``` +⚠️ 기존에 알고 있는 복권사업 도메인과 다를 수 있음. + +* 로또구매자 + - 로또를 사기 위해 돈을 지불한다. + - 발행 결과를 듣는다. + - 당첨 번호를 듣는다. + - 당첨 통계를 듣는다. + +* 로또판매자 + - 구매자의 돈을 받는다. + - 금액 판별기에 돈을 입력한다. + - 금액 판별기를 통해 발행 가능 여부를 얻는다. + - 발행이 가능하면 자동 로또번호 생성기에 발행 개수 n개를 입력한다. + - 발행 불가능하면, 로또를 발행하지 않는다. + - 자동 로또번호 생성기로 발행된 n개의 번호들을 기록장치에 기록한다. + - 구매자에게 발행된 개수와 번호들을 알린다. + - 로또운영위의 당첨번호를 기다린다. + - 구매자에게 당첨번호를 알린다. + - 당첨번호를 수령하면, 결과 판별기에게 당첨번호를 입력한다. + - 결과 판별기로부터 결과를 얻는다. + - 받은 금액과 판별 결과와 결과 분석기에 입력한다. + - 결과 분석 완료를 듣는다. + - 최종 결과를 구매자에게 알린다. + +* 금액판별기 + - 금액을 입력받는다. + - 유효한 금액인지 확인한다. + - 유효한 금액이면 발행 가능 개수를 판매자에게 알린다. + - 유효하지 않은 금액이면 발행 불가능함을 판매자에게 알린다. +* 자동 로또번호 생성기 + - 발행 개수를 입력받는다. + - 중복되지 않는 6개의 숫자를 생성한다. (1 ~ 45) + +* 기록 장치 + - 발행된 로또 개수(n)와(과) 각 로또 번호들을 기록한다. + - 결과판별기가 발행 개수와 로또 번호를 요청하면 이를 제공한다. + - 결과판별기가 당첨 결과를 도출하면 이를 기록한다. + - 결과분석기로부터 수익률을 받으면 이를 기록한다. + +* 로또운영위 + - 당첨 번호를 만든다. + - 6개의 숫자를 입력한다. (1 ~ 45) + - 보너스 숫자를 1개 입력한다. (1 ~ 45) + - 당첨 번호의 유효성을 확인한다. + - 유효한 당첨 번호이면 공개한다. + +* 결과판별기 + - 당첨 번호를 얻는다. + - 기록 장치에 발행 개수와 각 번호들을 요청한다. + - 당첨 번호와 생성 번호를 비교한다. + - 판별 결과를 도출한다. + - 판별 결과를 판매자에게 알린다. + +* 결과분석기 + - 판매자에게 금액과 결과지를 받는다. + - 최종 결과에 대한 수익률을 계산한다. (소수점 둘째 자리에서 반올림) + - 기록장치에 수익률을 알린다. + - 판매자에게 작업완료를 알린다. +``` + +## 예외처리 상황 + +``` +- 구매자가 금액을 지불할 때, 1000으로 나누어 떨어지지 않으면, 예외처리한다. +- 구매자가 금액을 지불할 때, 양수가 아니면 예외처리한다. +- 구매자가 금액을 지불할 때, Number.MAX_VALUE 보다 큰 입력이면 예외처리한다. +- 당첨번호를 입력할 때, 중복된 숫자를 입력하면 예외처리한다. +- 당첨번호를 입력할 때, 6개 숫자를 입력하지 않으면 예외처리한다. +- 당첨번호를 입력할 때, 1 ~ 45 사이의 수가 아니면 예외처리한다. +- 당첨번호를 입력할 때, 구분자로 ,가 입력되지 않으면 예외처리한다. +- 보너스번호를 입력할 때, 중복된 숫자를 입력하면 예외처리한다. +- 보너스번호를 입력할 때, 1개의 숫자를 입력하지 않으면 예외처리한다. +- 보너스번호를 입력할 때, 1 ~ 45 사이의 수가 아니면 예외처리한다. +``` diff --git a/src/App.js b/src/App.js index c38b30d5b2..394cfdb025 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,12 @@ +import LottoFlow from './controller/LottoFlow.js'; + class App { - async play() {} + #lottoFlow; + + async play() { + this.#lottoFlow = new LottoFlow(); + await this.#lottoFlow.makeLotto(); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e9..253e34a9ab 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,18 +1,33 @@ class Lotto { #numbers; - constructor(numbers) { + #bonusNumber; + + #ticketList; + + constructor(numbers, ticketList) { this.#validate(numbers); this.#numbers = numbers; + this.#ticketList = ticketList; } #validate(numbers) { if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); + } + const check = new Set(numbers); + if ([...check].length !== 6) { + throw new Error('[ERROR] 로또 번호는 중복이 불가능 합니다.'); } } - // TODO: 추가 기능 구현 + confirmNumber(bonusNumber) { + if (bonusNumber < 1 || bonusNumber > 45) { + throw new Error('[ERROR] 로또 번호는 1에서 45 사이 숫자만 가능합니다.'); + } + + this.#bonusNumber = bonusNumber; + } } export default Lotto; diff --git a/src/controller/LottoFlow.js b/src/controller/LottoFlow.js new file mode 100644 index 0000000000..8097a7a8b4 --- /dev/null +++ b/src/controller/LottoFlow.js @@ -0,0 +1,41 @@ +import InputView from '../views/InputView.js'; +import OutputView from '../views/OutputView.js'; +import Lotto from '../Lotto.js'; +import Seller from '../models/Seller.js'; +import ResultAnalyzer from '../models/ResultAnalyzer.js'; + +class LottoFlow { + #seller; + + #lotto; + + #resultAnalizer; + + constructor() { + this.#seller = new Seller(); + } + + async makeLotto() { + const money = await InputView.InputMoney(); + this.#seller.setAmount(money); + const { winningNumber, bonusNumber, ticketList } = + await this.#seller.makeWinnigNumber(); + await this.processLotto(winningNumber, bonusNumber, ticketList); + } + async processLotto(winningNumber, bonusNumber, ticketList) { + this.#lotto = new Lotto(winningNumber, ticketList); + await this.#lotto.confirmNumber(bonusNumber); + this.endLotto(winningNumber, bonusNumber, ticketList); + } + endLotto(winningNumber, bonusNumber, ticketList) { + this.#resultAnalizer = new ResultAnalyzer( + winningNumber, + bonusNumber, + ticketList, + ); + const result = this.#resultAnalizer.findResult(); + OutputView.printResult(result); + } +} + +export default LottoFlow; diff --git a/src/models/LottoMachine.js b/src/models/LottoMachine.js new file mode 100644 index 0000000000..d9a91d9a32 --- /dev/null +++ b/src/models/LottoMachine.js @@ -0,0 +1,19 @@ +import { Random } from '@woowacourse/mission-utils'; + +class LottoMachine { + constructor() { + this.ticket = []; + } + + makeTicket() { + while (this.ticket.length !== 6) { + const number = Random.pickNumberInRange(1, 45); + this.ticket.push(number); + const validTicket = new Set(this.ticket); + this.ticket = [...validTicket]; + } + return this.ticket; + } +} + +export default LottoMachine; diff --git a/src/models/ResultAnalyzer.js b/src/models/ResultAnalyzer.js new file mode 100644 index 0000000000..1b632fd040 --- /dev/null +++ b/src/models/ResultAnalyzer.js @@ -0,0 +1,70 @@ +class ResultAnalyzer { + #winningNumber; + + #bonusNumber; + + #ticketList; + + #prize; + + #profit; + + #reward; + + constructor(winningNumber, bonusNumber, ticketList) { + this.#winningNumber = winningNumber; + this.#bonusNumber = bonusNumber; + this.#ticketList = ticketList; + this.#prize = { + 3: 0, + 4: 0, + 5: 0, + bonus: 0, + 6: 0, + }; + this.#reward = [5000, 50000, 1500000, 30000000, 2000000000]; + this.#profit = 0; + } + + findResult() { + this.#ticketList.forEach((ticket) => { + this.countCorrect(ticket); + }); + this.calculateProfit(); + const result = { + prize: this.#prize, + profit: this.#profit, + }; + return result; + } + + countCorrect(ticket) { + let count = 0; + ticket.forEach((number) => { + if (this.#winningNumber.includes(number)) { + count += 1; + } + }); + if (count === 5 && ticket.includes(this.#bonusNumber)) { + this.#prize.bonus += 1; + } + if (count > 2) { + this.#prize[count] += 1; + } + } + + calculateProfit() { + const prizeNumber = Object.values(this.#prize); + const profit = prizeNumber.map( + (number, index) => number * this.#reward[index], + ); + const sum = profit.reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0, + ); + const pay = this.#ticketList.length * 1000; + this.#profit = ((sum / pay) * 100).toFixed(1); + } +} + +export default ResultAnalyzer; diff --git a/src/models/Seller.js b/src/models/Seller.js new file mode 100644 index 0000000000..9f6a321347 --- /dev/null +++ b/src/models/Seller.js @@ -0,0 +1,44 @@ +import InputView from '../views/InputView.js'; +import OutputView from '../views/OutputView.js'; +import LottoMachine from './LottoMachine.js'; + +class Seller { + #lottoMachine; + + #winningNumber; + + #bonusNumber; + + constructor() { + this.ticketList = []; + this.result = []; + } + + setAmount(money) { + const amount = money / 1000; + OutputView.printAmount(amount); + this.setLottoTicket(amount); + } + + setLottoTicket(amount) { + for (let order = 0; order < amount; order += 1) { + this.#lottoMachine = new LottoMachine(); + const ticket = this.#lottoMachine.makeTicket(); + this.ticketList.push(ticket); + } + OutputView.printTickets(this.ticketList); + } + + async makeWinnigNumber() { + this.#winningNumber = await InputView.InputWinningNumber(); + this.#bonusNumber = await InputView.InputBonusNumber(); + const NumberSet = { + winningNumber: this.#winningNumber, + bonusNumber: this.#bonusNumber, + ticketList: this.ticketList, + }; + return NumberSet; + } +} + +export default Seller; diff --git a/src/test.js b/src/test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000000..ec506a8666 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,9 @@ +const validate = { + isValidateMoney(money) { + if (money % 1000 !== 0) { + throw new Error('[ERROR] 잘못된 입력입니다.'); + } + }, +}; + +export default validate; diff --git a/src/views/InputView.js b/src/views/InputView.js new file mode 100644 index 0000000000..b06b127648 --- /dev/null +++ b/src/views/InputView.js @@ -0,0 +1,30 @@ +import { Console } from '@woowacourse/mission-utils'; +import validate from '../utils/validate.js'; + +const InputView = { + async InputMoney() { + const money = parseInt( + await Console.readLineAsync('구매금액을 입력해 주세요.\n'), + 10, + ); + validate.isValidateMoney(money); + return money; + }, + async InputWinningNumber() { + const winningInput = await Console.readLineAsync( + '\n당첨 번호를 입력해 주세요.\n', + ); + const winningArray = winningInput.split(','); + const winningNumber = winningArray.map((number) => parseInt(number, 10)); + return winningNumber; + }, + async InputBonusNumber() { + const bonusNumber = parseInt( + await Console.readLineAsync('\n보너스 번호를 입력해 주세요.\n'), + 10, + ); + return bonusNumber; + }, +}; + +export default InputView; diff --git a/src/views/OutputView.js b/src/views/OutputView.js new file mode 100644 index 0000000000..5db09fc968 --- /dev/null +++ b/src/views/OutputView.js @@ -0,0 +1,20 @@ +import { Console } from '@woowacourse/mission-utils'; + +const OutputView = { + printAmount(amount) { + Console.print(`\n${amount}개를 구매했습니다.`); + }, + printTickets(ticketList) { + ticketList.forEach((ticket) => { + Console.print(ticket); + }); + }, + printResult(result) { + const { prize, profit } = result; + Console.print( + `\n당첨 통계\n---\n3개 일치 (5,000원) - ${prize[3]}개\n4개 일치 (50,000원) - ${prize[4]}개\n5개 일치 (1,500,000원) - ${prize[5]}개\n5개 일치, 보너스 볼 일치 (30,000,000원) - ${prize.bonus}개\n6개 일치 (2,000,000,000원) - ${prize[6]}개\n총 수익률은 ${profit}%입니다.`, + ); + }, +}; + +export default OutputView;