diff --git a/README.md b/README.md index 15bb106b5..57f9d1059 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ # javascript-lotto-precourse + +## 기능 목록 + +1. 사용자로부터 로또 구매 금액을 입력 받는다. + - 입력된 금액의 유효성을 검사한다. + +2. 발급된 로또 수량 및 오름차순으로 정렬된 로또 번호를 출력한다. + +3. 쉼표(,)를 기준으로 당첨 번호를 입력 받는다. + - 입력된 당첨 번호의 유효성을 검사한다. + +4. 사용자로부터 보너스 번호를 입력 받는다. + - 보너스 번호의 유효성을 검사한다. + +5. 당첨 통계 정보를 출력한다. + +6. 총 수익률을 소수점 둘째 자리에서 반올림하여 표시한다. + +--- + +## 예외 처리 사항 + +### 보너스 번호 검증 +- 입력 값은 1부터 45 사이의 숫자여야 합니다. +- 당첨 번호와 중복되어서는 안 됩니다. +- 단일 숫자만 입력할 수 있습니다. + +### 당첨 번호 검증 +- 모든 입력 번호는 1부터 45 사이의 숫자여야 합니다. +- 번호는 중복되지 않도록 해야 합니다. +- 각 번호는 쉼표로 구분하여 입력해야 합니다. +- 총 6개의 번호를 입력해야 합니다. + +### 로또 구매 금액 검증 +- 입력 값은 반드시 양수여야 합니다. +- 금액은 1000원 단위로 입력해야 합니다. +- 하나의 숫자만 입력해야 합니다. diff --git a/src/App.js b/src/App.js index 091aa0a5d..476d11d0f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,9 @@ +import LottoActions from "./LottoActions.js"; + class App { - async run() {} + async run() { + await new LottoActions().play(); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..92cc20d70 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,18 +1,48 @@ +import { BONUS_MATCH_RANK_INDEX, LOTTO_MATCH_COUNT } from "./constants/lottoNumbers.js"; +import InputValidate from "./utils/InputValidate.js"; + +const RANK = [ + "firstPlace", + "thirdPlace", + "fourthPlace", + "fifthPlace", + "blank", + "blank", + "blank", + "secondPlace", +]; + + + class Lotto { #numbers; constructor(numbers) { + this.error = new InputValidate(); this.#validate(numbers); this.#numbers = numbers; } #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); - } + this.error.inputExist(numbers); + this.error.lottoNumberLength(numbers); + this.error.lottoNumberRange(numbers); + this.error.lottoNumberType(numbers); + this.error.duplicateLottoNumber(numbers); } - // TODO: 추가 기능 구현 + getLottoNumber() { + return this.#numbers; + } + + compareLotto(winningNumber, bonusNumber) { + + const count = this.#numbers.filter((value) => winningNumber.includes(value)).length; + if(this.#numbers.includes(bonusNumber) && count === LOTTO_MATCH_COUNT){ + return RANK[BONUS_MATCH_RANK_INDEX]; + } + return RANK[this.#numbers.length - count]; + } } -export default Lotto; +export default Lotto; \ No newline at end of file diff --git a/src/LottoActions.js b/src/LottoActions.js new file mode 100644 index 000000000..5bdabb3ec --- /dev/null +++ b/src/LottoActions.js @@ -0,0 +1,149 @@ +import { Console } from "@woowacourse/mission-utils"; +import InputValidate from "./utils/InputValidate.js"; +import LottoModel from "./model/lottoModel.js"; + +class LottoActions { + constructor() { + this.error = new InputValidate(); + this.model = new LottoModel(); + } + + async play() { + await this.issueLotto(); + this.getInformation(); + } + + async issueLotto() { + const userPrice = await this.inputPrice(); + this.getLottoList(userPrice); + + const winningNumber = await this.inputWinningNumber(); + this.model.setWinningNumber(winningNumber); + + const bonusNumber = await this.inputBonusNumber(); + this.model.setBonusNumber(bonusNumber); + } + + getInformation() { + this.model.setWinningStatistics(); + this.printResultMessage(); + this.printUserRank(this.model.getStatistics()); + const profit = this.model.calculateProfit(); + this.printProfit(profit); + } + + async inputPrice() { + let isValid = false; + let price; + do { + price = await Console.readLineAsync("구입금액을 입력해 주세요."); + isValid = this.validateUserPrice(price); + } while (!isValid); + return price; + } + + validateUserPrice(price) { + const validationMessage = this.error.priceInputValidate(price); + if (validationMessage) { + Console.print(validationMessage); + return false; + } + return true; + } + + getLottoList(userPrice) { + this.model.setPrice(userPrice); + this.model.generateLottoNumber(); + const lottoList = this.model.getLottoList(); + this.printLottoList(lottoList); + } + + async inputWinningNumber() { + let isValid = false; + let winningNumber; + do { + winningNumber = await Console.readLineAsync( + "\n당첨 번호를 입력해 주세요.\n" + ); + isValid = this.validateWinningNumber(winningNumber.split(",")); + } while (!isValid); + return winningNumber.split(",").map(Number); + } + + validateWinningNumber(numbers) { + const validationMessage = this.error.lottoNumberValidate(numbers); + if (validationMessage) { + Console.print(validationMessage); + return false; + } + return true; + } + + async inputBonusNumber() { + let bonusNumber; + let isValid = false; + do { + bonusNumber = await Console.readLineAsync( + "\n보너스 번호를 입력해 주세요.\n" + ); + isValid = this.validateBonusNumber(bonusNumber); + } while (!isValid); + return Number(bonusNumber); + } + + validateBonusNumber(bonusNumber) { + const validationMessage = this.error.bonusNumberValidate( + bonusNumber, + this.model.getWinningNumber() + ); + if (validationMessage) { + Console.print(validationMessage); + return false; + } + return true; + } + + printLottoList(lottoList) { + Console.print(`\n${lottoList.length}개를 구매했습니다.`); + for (let lotto of lottoList) { + Console.print(`[${lotto.getLottoNumber().join(", ")}]`); + } + } + + printResultMessage() { + Console.print("\n당첨 통계"); + Console.print("---"); + } + + printUserRank(userDetails) { + const [fifth, fourth, third, second, first] = [ + userDetails.fifthPlace || 0, + userDetails.fourthPlace || 0, + userDetails.thirdPlace || 0, + userDetails.secondPlace || 0, + userDetails.firstPlace || 0, + ]; + + const lottoArray = [ + `3개 일치 (5,000원) - ${fifth}개`, + `4개 일치 (50,000원) - ${fourth}개`, + `5개 일치 (1,500,000원) - ${third}개`, + `5개 일치, 보너스 볼 일치 (30,000,000원) - ${second}개`, + `6개 일치 (2,000,000,000원) - ${first}개`, + ]; + + this.printUserLotto(lottoArray); + } + + printUserLotto(lottoArray) { + for (let message of lottoArray) { + Console.print(message); + } + } + + printProfit(profit) { + Console.print(`총 수익률은 ${profit}%입니다.`); + } +} + +export default LottoActions; diff --git a/src/constants/error.js b/src/constants/error.js new file mode 100644 index 000000000..ec37781c3 --- /dev/null +++ b/src/constants/error.js @@ -0,0 +1,13 @@ +export const ERROR_MESSAGE = { + EMPTY_INPUT: "[ERROR] 입력값이 비어 있습니다. 값을 입력해 주세요.", + INVALID_PRICE_TYPE: "[ERROR] 유효한 숫자를 입력해 주세요.", + INVALID_PRICE_UNIT: "[ERROR] 금액은 1,000원 단위로 입력해 주세요.", + NEGATIVE_PRICE: "[ERROR] 금액은 양수로 입력해 주세요.", + INVALID_LOTTO_RANGE: "[ERROR] 당첨 번호는 1에서 45 사이의 숫자여야 합니다.", + LOTTO_NUMBER_DUPLICATE: "[ERROR] 당첨 번호는 중복되지 않도록 입력해 주세요.", + INVALID_LOTTO_LENGTH: "[ERROR] 로또 번호는 6개의 숫자여야 합니다.", + INVALID_LOTTO_TYPE: "[ERROR] 숫자를 쉼표(,)로 구분하여 입력해 주세요.", + INVALID_BONUS_NUMBER_TYPE: "[ERROR] 보너스 번호는 하나의 숫자로 입력해 주세요.", + INVALID_BONUS_NUMBER_RANGE: "[ERROR] 보너스 번호는 1에서 45 사이의 숫자여야 합니다.", + BONUS_NUMBER_DUPLICATE: "[ERROR] 보너스 번호는 당첨 번호와 중복되지 않도록 입력해 주세요.", +}; diff --git a/src/constants/lottoNumbers.js b/src/constants/lottoNumbers.js new file mode 100644 index 000000000..42e05d477 --- /dev/null +++ b/src/constants/lottoNumbers.js @@ -0,0 +1,11 @@ +export const LOTTO_NUMBER_RANGE = { + MINIMUM : 1, + MAXIMUM : 45, + COUNT : 6, +} + +export const PRICE_UNIT = 1000; + +export const LOTTO_MATCH_COUNT = 5; + +export const BONUS_MATCH_RANK_INDEX = 7; \ No newline at end of file diff --git a/src/model/lottoModel.js b/src/model/lottoModel.js new file mode 100644 index 000000000..4a7081e9b --- /dev/null +++ b/src/model/lottoModel.js @@ -0,0 +1,78 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import Lotto from "../Lotto.js"; +import { LOTTO_NUMBER_RANGE, PRICE_UNIT } from "../constants/lottoNumbers.js"; + +const PRICE = { + firstPlace : 2000000000, + secondPlace : 30000000, + thirdPlace : 1500000, + fourthPlace : 50000, + fifthPlace : 5000 +} + +class LottoModel{ + #userPrice + #winningNumber + #bonusNumber + + constructor() { + this.#userPrice = 0; + this.lottoList = []; + this.#winningNumber = []; + this.#bonusNumber; + this.userDetails = { + fifthPlace : 0, + fourthPlace : 0, + thirdPlace : 0, + secondPlace : 0, + firstPlace : 0, + } + } + setPrice(price) { + this.#userPrice = price; + } + + generateLottoNumber() { + const numberOfLotto = this.#userPrice/PRICE_UNIT; + for (let i = 0; i < numberOfLotto; i++) { + let number = MissionUtils.Random.pickUniqueNumbersInRange(LOTTO_NUMBER_RANGE.MINIMUM, LOTTO_NUMBER_RANGE.MAXIMUM, LOTTO_NUMBER_RANGE.COUNT).sort((a, b) => a - b); + let lotto = new Lotto(number); + this.lottoList.push(lotto); + } + } + getLottoList() { + return this.lottoList; + } + + setWinningNumber(numbers) { + this.#winningNumber = numbers; + } + getWinningNumber() { + return this.#winningNumber; + } + setBonusNumber(bonusNumber) { + this.#bonusNumber = bonusNumber; + } + setWinningStatistics() { + this.lottoList.forEach((lotto) => { + let lottoRank = lotto.compareLotto(this.#winningNumber, this.#bonusNumber); + if(lottoRank in this.userDetails){ + this.userDetails[lottoRank] += 1; + } + }) + } + getStatistics() { + return this.userDetails; + } + + calculateProfit() { + let total = 0; + for (const [key, count] of Object.entries(this.userDetails)) { + total += PRICE[key]*count; + }; + const profit = (total/this.#userPrice)*100; + return Math.round(profit*100)/100; + } +} + +export default LottoModel; \ No newline at end of file diff --git a/src/utils/InputValidate.js b/src/utils/InputValidate.js new file mode 100644 index 000000000..811df3b14 --- /dev/null +++ b/src/utils/InputValidate.js @@ -0,0 +1,106 @@ +import {ERROR} from "../constants/error.js"; + +class InputValidate{ + inputExist(input) { + if (!input){ + throw new Error(ERROR.EMPTY_INPUT); + } + } + inputType(input) { + if (isNaN(Number(input))){ + throw new Error(ERROR.INVALID_PRICE_TYPE); + } + } + inputUnit(input) { + if (input%1000 !== 0){ + throw new Error(ERROR.INVALID_PRICE_UNIT); + } + } + inputRange(input) { + if(input<0) { + throw new Error(ERROR.INVALID_LOTTO_RANGE); + } + } + lottoNumberRange(input) { + for (let number of input){ + if(number > 45 || number < 1){ + throw new Error(ERROR.INVALID_LOTTO_RANGE); + } + } + } + duplicateLottoNumber(input) { + const noDuplicate = new Set(input); + if(input.length !== noDuplicate.size){ + throw new Error(ERROR.INVALID_LOTTO_LENGTH); + } + } + lottoNumberLength(input) { + if (input.length !== 6){ + throw new Error(ERROR.INVALID_LOTTO_LENGTH); + } + } + + lottoNumberType(input) { + for (let number of input){ + if (isNaN(Number(number))){ + throw new Error(ERROR.INVALID_LOTTO_TYPE); + } + } + } + + bonusNumberType(input) { + if (isNaN(Number(input))){ + throw new Error(ERROR.INVALID_BONUS_NUMBER_TYPE); + } + } + bonusNumberRange(input) { + if (input > 45 || input < 1){ + throw new Error(ERROR.INVALID_BONUS_NUMBER_RANGE); + } + } + duplicateBonusNumber(bonusNumber, lottoNumber) { + if (lottoNumber.includes(bonusNumber)) { + throw new Error(ERROR.BONUS_NUMBER_DUPLICATE); + } + } + + priceInputValidate(input) { + try{ + this.inputExist(input); + this.inputType(input); + this.inputUnit(input); + this.inputRange(input); + return null; + } catch(error) { + return error.message; + } + + } + + lottoNumberValidate(input) { + try { + this.inputExist(input); + this.lottoNumberRange(input); + this.lottoNumberLength(input); + this.duplicateLottoNumber(input); + this.lottoNumberType(input); + return null; + } catch(error) { + return error.message; + } + } + + bonusNumberValidate(bonusNumber, lottoNumber) { + try { + this.inputExist(bonusNumber); + this.duplicateBonusNumber(bonusNumber, lottoNumber); + this.bonusNumberType(bonusNumber); + this.bonusNumberRange(bonusNumber); + return null; + } catch(error) { + return error.message; + } + } +}; + +export default InputValidate;