diff --git a/README.md b/README.md index 15bb106b5..384410e86 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ # javascript-lotto-precourse + +  + +  + +# ✅ 구현할 기능 목록 + +간단한 로또 발매기를 구현한다. + +  + +### 🟢 [입력] 로또 구입 금액을 입력받는다. + +#### ㄴ validation + +- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다. +- 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나우어 떨어지지 않는 경우 예외 처리한다. + +  + +  + +### 🟢 [출력] 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다. + +  + +  + +### 🟢 [입력] 당첨번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다. + +#### ㄴ validation + +- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다. + +  + +  + +### 🟢 [입력] 보너스 번호를 입력 받는다. + +#### ㄴ validation + +- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다. + +  + +  + +### 🟢 [출력] 당첨 내역을 출력한다. + +<출력 예시> +당첨 통계 + +\--- + +3개 일치 (5,000원) - 1개 + +  + +  + +### 🟢 [출력] 총 수익률을 출력한다. + +수익률은 소수점 둘쨰 자리에서 반올림 한다. (ex. 100.0%, 51.5%, 1,000,000.0%) + +- diff --git a/src/App.js b/src/App.js index 091aa0a5d..8b0404c4d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,9 @@ +import LottoController from './controller/LottoController.js'; + class App { - async run() {} + async run() { + await new LottoController().runLotto(); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..1164a3f81 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -8,7 +8,7 @@ class Lotto { #validate(numbers) { if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); } } diff --git a/src/constants/errorMessages.js b/src/constants/errorMessages.js new file mode 100644 index 000000000..6ba8406db --- /dev/null +++ b/src/constants/errorMessages.js @@ -0,0 +1,20 @@ +const ERROR_PREFIX = '[ERROR]'; + +const createMsg = (msg) => `${ERROR_PREFIX} ${msg} ${'다시 입력해주세요.'}\n`; + +const ERROR_MESSAGES = { + emptyValue: createMsg('값이 존재하지 않습니다.'), + regexpTest: createMsg('형식이 잘 못되었습니다.'), + startComma: createMsg('입력이 콤마(,)부터 시작할 수 없습니다.'), + endComma: createMsg('입력 끝에 콤마(,)로 끝날 수 없습니다.'), + enteredMoreFiveTimes: '5회 이상 잘못 입력하여 종료되없습니다. 다시 실행해주세요.', + limitDigits: createMsg('최소 4자리 숫자부터 6자리 숫자까지 입력 가능합니다.'), + negativeNumber: createMsg('음수가 입력될 수 없습니다.'), + thousandUnit: createMsg( + '로또 금액인 1000원 단위로만 입력 가능하며, 최대 10만원까지 입력 가능합니다.' + ), + winningNumberSixDigit: createMsg('당첨번호는 6자리를 입력해야합니다.'), + isDuplicatedInWinningNumber: createMsg('당첨번호와 중복되는 번호를 입력하셨습니다.'), +}; + +export { ERROR_PREFIX, ERROR_MESSAGES }; diff --git a/src/constants/inputMessages.js b/src/constants/inputMessages.js new file mode 100644 index 000000000..b53bd6112 --- /dev/null +++ b/src/constants/inputMessages.js @@ -0,0 +1,7 @@ +const INPUT_MESSAGES = { + whatPurchaseAmount: '구매금액을 입력해주세요. \n => ', + winningNumber: '당첨번호를 입력해주세요. \n => ', + bonusNumber: '보너스번호를 입력해주세요. \n => ', +}; + +export default INPUT_MESSAGES; diff --git a/src/constants/outputMessages.js b/src/constants/outputMessages.js new file mode 100644 index 000000000..ed9e74e1f --- /dev/null +++ b/src/constants/outputMessages.js @@ -0,0 +1,14 @@ +const OUTPUT_MESSAGES = { + purchaseNumber: '개를 구매했습니다.', + winningStatistics: '당첨 통계\n--- ', + matchedThree: '3개 일치 (5,000원) - ', + matchedFour: '4개 일치 (50,000원) - ', + matchedFive: '5개 일치 (1,500,000원) - ', + matchedFiveBonus: '5개 일치, 보너스 볼 일치 (30,000,000원) - ', + matchedSix: '6개 일치 (2,000,000,000원) - ', + printQuantity: '개', + totalReturn: '총 수익률은 ', + totalReturnPercentage: '% 입니다.', +}; + +export default OUTPUT_MESSAGES; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 000000000..adf0063d8 --- /dev/null +++ b/src/controller/LottoController.js @@ -0,0 +1,26 @@ +import InputPurchaseModule from '../modules/InputPurchaseModule.js'; +import InputWinningModule from '../modules/InputWinningModule.js'; +import InputBonusModule from '../modules/InputBonusModule.js'; +import PrintWinningDetails from '../modules/printWinningDetails.js'; +import PrintRandomNumber from '../modules/PrintRandomNumber.js'; + +class LottoController { + constructor() { + this.inputPurchaseModule = new InputPurchaseModule(); + this.inputWinningModule = new InputWinningModule(); + this.printRandomNumber = new PrintRandomNumber(); + } + + async runLotto() { + const purchasPrice = await this.inputPurchaseModule.inputPurchaseAmount(); + const purchasQuantity = purchasPrice / 1000; + const lottoNumber = this.printRandomNumber.printRandomNumber(purchasQuantity); + const winningNumber = await this.inputWinningModule.inputWinningNumber(); + const inputBonusModule = new InputBonusModule(winningNumber); + const bonusNumber = await inputBonusModule.inputBonusNumber(); + const printWinningDetails = new PrintWinningDetails(purchasPrice); + printWinningDetails.winningDetails(lottoNumber, winningNumber, bonusNumber); + } +} + +export default LottoController; diff --git a/src/index.js b/src/index.js index 02a1d389e..9daefc93f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import App from "./App.js"; +import App from './App.js'; const app = new App(); await app.run(); diff --git a/src/modules/InputBonusModule.js b/src/modules/InputBonusModule.js new file mode 100644 index 000000000..20b5d5370 --- /dev/null +++ b/src/modules/InputBonusModule.js @@ -0,0 +1,52 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import validation from '../validation/validation.js'; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; + +class InputBonusModule { + constructor(winningNumber) { + this.validation = new validation(); + this.winningNumber = winningNumber; + } + + async inputBonusNumber() { + const validatedBonusNumber = await this.repeatInput(); + const changeTypeNumber = Number(validatedBonusNumber); + OutputView.printSpace(); + return changeTypeNumber; + } + + async inputAndValidation() { + const input = await InputView.readBonusNumber(); + const winningNumber = this.winningNumber; + this.validateBonusNumber(input, winningNumber); + return input; + } + + validateBonusNumber(value, winningNumber) { + this.validation.empty(value); + const regExpPattern = /\d{1,2}/; + this.validation.regularExpression(value, regExpPattern); + this.validation.isDuplicatedInWinningNumber(value, winningNumber); + } + + async errorCatch() { + try { + const validatedInput = await this.inputAndValidation(); + return validatedInput; + } catch (error) { + OutputView.printError(error); + return false; + } + } + + async repeatInput() { + for (let i = 0; i < 10; i++) { + const vlaidatedInput = await this.errorCatch(); + if (vlaidatedInput) return vlaidatedInput; + if (i === 5) throw new Error(ERROR_MESSAGES.enteredMoreFiveTimes); + } + } +} + +export default InputBonusModule; diff --git a/src/modules/InputPurchaseModule.js b/src/modules/InputPurchaseModule.js new file mode 100644 index 000000000..3d13832c8 --- /dev/null +++ b/src/modules/InputPurchaseModule.js @@ -0,0 +1,52 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import validation from '../validation/validation.js'; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; + +class InputPurchaseModule { + constructor() { + this.validation = new validation(); + } + + async inputPurchaseAmount() { + const validatedPurchaseAmount = await this.repeatInput(); + const changgeTypeNumber = Number(validatedPurchaseAmount); + OutputView.printSpace(); + return changgeTypeNumber; + } + + async inputAndValidation() { + const input = await InputView.readPurchaseAmount(); + this.validatatePrice(input); + return input; + } + + validatatePrice(value) { + this.validation.empty(value); + const regExpPattern = /\d/; + this.validation.regularExpression(value, regExpPattern); + this.validation.nagativeNumber(value); + this.validation.limitDigits(value); + this.validation.thousandUnit(value); + } + + async errorCatch() { + try { + const validatedInput = await this.inputAndValidation(); + return validatedInput; + } catch (error) { + OutputView.printError(error); + return false; + } + } + + async repeatInput() { + for (let i = 0; i < 10; i++) { + const vlaidatedInput = await this.errorCatch(); + if (vlaidatedInput) return vlaidatedInput; + if (i === 5) throw new Error(ERROR_MESSAGES.enteredMoreFiveTimes); + } + } +} + +export default InputPurchaseModule; diff --git a/src/modules/InputWinningModule.js b/src/modules/InputWinningModule.js new file mode 100644 index 000000000..2ae1fd382 --- /dev/null +++ b/src/modules/InputWinningModule.js @@ -0,0 +1,51 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import validation from '../validation/validation.js'; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; + +class InputWinningModule { + constructor() { + this.validation = new validation(); + } + async inputWinningNumber() { + const validatedLottoNumber = await this.repeatInput(); + const changgeTypeArray = validatedLottoNumber.split(',').map((string) => Number(string)); + OutputView.printSpace(); + return changgeTypeArray; + } + + async inputAndValidation() { + const input = await InputView.readWinningNumber(); + this.validateWinningNumber(input); + return input; + } + + validateWinningNumber(value) { + this.validation.empty(value); + this.validation.startedComma(value); + this.validation.endedComma(value); + const regExpPattern = /^\d(,\s?\d)*/; + this.validation.regularExpression(value, regExpPattern); + this.validation.winningNumberSixDigit(value); + } + + async errorCatch() { + try { + const validatedInput = await this.inputAndValidation(); + return validatedInput; + } catch (error) { + OutputView.printError(error); + return false; + } + } + + async repeatInput() { + for (let i = 0; i < 10; i++) { + const vlaidatedInput = await this.errorCatch(); + if (vlaidatedInput) return vlaidatedInput; + if (i === 5) throw new Error(ERROR_MESSAGES.enteredMoreFiveTimes); + } + } +} + +export default InputWinningModule; diff --git a/src/modules/PrintRandomNumber.js b/src/modules/PrintRandomNumber.js new file mode 100644 index 000000000..9b560ddf8 --- /dev/null +++ b/src/modules/PrintRandomNumber.js @@ -0,0 +1,19 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import OutputView from '../view/OutputView.js'; + +class PrintRandomNumber { + printRandomNumber(purchasQuantity) { + OutputView.printPurchaseNumber(purchasQuantity); + let lottoNumberArrayss = []; + for (let i = 0; i < purchasQuantity; i++) { + const lottoNumber = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6); + const ascendingOrder = lottoNumber.sort((a, b) => a - b); + OutputView.printLottoNumber(ascendingOrder); + lottoNumberArrayss = [...lottoNumberArrayss, ascendingOrder]; + } + OutputView.printSpace(); + return lottoNumberArrayss; + } +} + +export default PrintRandomNumber; diff --git a/src/modules/PrintWinningDetails.js b/src/modules/PrintWinningDetails.js new file mode 100644 index 000000000..6b2e2eeda --- /dev/null +++ b/src/modules/PrintWinningDetails.js @@ -0,0 +1,73 @@ +import OutputView from '../view/OutputView.js'; + +class PrintWinningDetails { + constructor(purchasePrice) { + this.purchasePrice = purchasePrice; + } + + winningDetails(lottoNumber, winningNumber, bonusNumber) { + const commonNumbers = this.checkMatchedNumber(lottoNumber, winningNumber, bonusNumber); + // console.log(commonNumbers); + const winningStatistics = this.getWinningStatistics(commonNumbers); + this.printWinningStatistics(winningStatistics); + const winningAmount = this.getWinningAmount(winningStatistics); + const purchasePrice = this.purchasePrice; + this.printProfitRate(purchasePrice, winningAmount); + } + + // service logic + checkMatchedNumber(lottoNumber, winningNumber, bonusNumber) { + let commonNumbers = []; + lottoNumber.map((lottoArray) => { + const matchedNumbers = lottoArray.filter((array) => winningNumber.includes(array)).length; + const matchedBonus = lottoArray.filter((array) => [bonusNumber].includes(array)).length; + const commonResult = { matchNum: matchedNumbers, matchBonus: matchedBonus }; + return (commonNumbers = [...commonNumbers, commonResult]); + }); + return commonNumbers; + } + + getWinningStatistics(commonNumbers) { + const winningStatistics = [ + { matchThree: commonNumbers.filter((o) => o.matchNum === 3).length }, + { matchFour: commonNumbers.filter((o) => o.matchNum === 4).length }, + { matchFive: commonNumbers.filter((o) => o.matchNum === 5 && o.matchBonus === 0).length }, + { + matchFiveAndBonus: commonNumbers.filter((o) => o.matchNum === 5 && o.matchBonus === 1) + .length, + }, + { matchSix: commonNumbers.filter((o) => o.matchNum === 6).length }, + ]; + return winningStatistics; + } + + printWinningStatistics(winningStatistics) { + OutputView.printWinningMessage(); + OutputView.printWinningStatistics( + winningStatistics[0].matchThree, + winningStatistics[1].matchFour, + winningStatistics[2].matchFive, + winningStatistics[3].matchFiveAndBonus, + winningStatistics[4].matchSix + ); + } + + getWinningAmount(winningStatistics) { + const calculThree = winningStatistics[0].matchThree * 5000; + const calculFour = winningStatistics[1].matchFour * 50000; + const calculFive = winningStatistics[2].matchFive * 1500000; + const calculFiveAndBonus = winningStatistics[3].matchFiveAndBonus * 30000000; + const calculSix = winningStatistics[4].matchSix * 2000000000; + const sum = calculThree + calculFour + calculFive + calculFiveAndBonus + calculSix; + return sum; + } + + printProfitRate(purchasePrice, getWinningAmount) { + const profitRateCalcul = (((getWinningAmount - purchasePrice) / purchasePrice) * 100).toFixed( + 2 + ); + OutputView.printTotalReturn(profitRateCalcul); + } +} + +export default PrintWinningDetails; diff --git a/src/utils/createError.js b/src/utils/createError.js new file mode 100644 index 000000000..1854052d7 --- /dev/null +++ b/src/utils/createError.js @@ -0,0 +1,5 @@ +const createThrowError = (message) => { + throw new Error(`${message}`); +}; + +export default createThrowError; diff --git a/src/validation/validation.js b/src/validation/validation.js new file mode 100644 index 000000000..05a88f0ba --- /dev/null +++ b/src/validation/validation.js @@ -0,0 +1,66 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import createThrowError from '../utils/createError.js'; + +class validation { + empty(value) { + if (value === '') createThrowError(ERROR_MESSAGES.emptyValue); + return true; + } + + regularExpression(value, regExp) { + const regExpTest = regExp.test(value); + if (!regExpTest) createThrowError(ERROR_MESSAGES.regexpTest); + return true; + } + + startedComma(value) { + const regExp = /^,/; + const regExpTest = regExp.test(value); + if (regExpTest) createThrowError(ERROR_MESSAGES.startComma); + return true; + } + + endedComma(value) { + const regExp = /,$/; + const regExpTest = regExp.test(value); + if (regExpTest) createThrowError(ERROR_MESSAGES.endComma); + return true; + } + + nagativeNumber(value) { + const changeTypeNumber = Number(value); + if (0 > changeTypeNumber) createThrowError(ERROR_MESSAGES.negativeNumber); + return true; + } + + limitDigits(value) { + const regExp = /\d{4,6}$/; + const regExpTest = regExp.test(value); + if (!regExpTest) createThrowError(ERROR_MESSAGES.limitDigits); + return true; + } + + thousandUnit(value) { + const changeTypeNumber = Number(value); + const target = changeTypeNumber / 1000; + const isInteger = Number.isInteger(target); + if (!isInteger) createThrowError(ERROR_MESSAGES.thousandUnit); + if (0 < target && target > 101) createThrowError(ERROR_MESSAGES.THOUSAND_UNIT); + return true; + } + + winningNumberSixDigit(value) { + const lottoNumberArray = value.split(','); + if (lottoNumberArray.length !== 6) createThrowError(ERROR_MESSAGES.winningNumberSixDigit); + return true; + } + + isDuplicatedInWinningNumber(bonusNumber, winningNumber) { + const isDuplicated = winningNumber.some( + (winningNumber) => Number(winningNumber) === Number(bonusNumber) + ); + if (isDuplicated) createThrowError(ERROR_MESSAGES.isDuplicatedInWinningNumber); + } +} + +export default validation; diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 000000000..23af82e78 --- /dev/null +++ b/src/view/InputView.js @@ -0,0 +1,18 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import INPUT_MESSAGES from '../constants/inputMessages.js'; + +const InputView = { + async readPurchaseAmount() { + return await MissionUtils.Console.readLineAsync(INPUT_MESSAGES.whatPurchaseAmount); + }, + + async readWinningNumber() { + return await MissionUtils.Console.readLineAsync(INPUT_MESSAGES.winningNumber); + }, + + async readBonusNumber() { + return await MissionUtils.Console.readLineAsync(INPUT_MESSAGES.bonusNumber); + }, +}; + +export default InputView; diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 000000000..19bf8e2d1 --- /dev/null +++ b/src/view/OutputView.js @@ -0,0 +1,38 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import OUTPUT_MESSAGES from '../constants/outputMessages.js'; + +const OutputView = { + async printPurchaseNumber(purchasQuantity) { + MissionUtils.Console.print(`${purchasQuantity}${OUTPUT_MESSAGES.purchaseNumber}`); + }, + + async printWinningMessage() { + MissionUtils.Console.print(OUTPUT_MESSAGES.winningStatistics); + }, + + async printWinningStatistics(three, four, five, fiveAndBonus, six) { + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedThree}${three}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedFour}${four}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedFive}${five}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedFiveBonus}${fiveAndBonus}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedSix}${six}${OUTPUT_MESSAGES.printQuantity}`); + }, + + async printTotalReturn(percentage) { + MissionUtils.Console.print(`${OUTPUT_MESSAGES.totalReturn}${percentage}${OUTPUT_MESSAGES.totalReturnPercentage}`); + }, + + async printLottoNumber(value) { + MissionUtils.Console.print(value); + }, + + async printError(error) { + MissionUtils.Console.print(error); + }, + + async printSpace() { + MissionUtils.Console.print(''); + }, +}; + +export default OutputView;