diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..670447f603 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,15 @@ +{ + "extends": "airbnb-base", + "parserOptions": { + "ecmaVersion": 13 + }, + "rules": { + "import/extensions": [ + "error", + "always", + { + "js": "always" + } + ] + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..2857e5cf03 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,74 @@ +## 흐름파악 + +![Alt text](image.png) + +## 기능구현 목록 + +### 유저스토리 + +1. 유저는 구입 금액을 입력할 수 있다. + +- 1000원 단위로 받는다 (여기서 소수점도 걸림) +- 숫자만 입력해야된다 +- 양의 정수만 입력해야된다 + +2. 유저는 당첨 번호를 입력할 수 있다. + +- 각 1~45 범위의 숫자를 입력해야된다. +- 쉼표(,)기준으로 구분한다. +- 6개를 입력해야 한다. +- 중복되지 않아야 한다. + +3. 유저는 보너스 번호를 입력할 수 있다. + +- 1~45 범위의 숫자 한개만 입력할 수 있다. + +4. 유저는 구입한 금액만큼 발행한 로또의 수량과 번호를 볼 수있다. + +- 구입한 금액 만큼 로또를 생성해야 한다. +- 오름차순이어야 한다. +- 갯수를 보여줘야된다. +- 배열 형태로 보여준다. + +5. 유저는 당첨 내역과 수익률을 볼 수 있다. + +기준은 다음과 같다 + +- 1등: 6개 번호 일치 / 2,000,000,000원 + + - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 + - 3등: 5개 번호 일치 / 1,500,000원 + - 4등: 4개 번호 일치 / 50,000원 + - 5등: 3개 번호 일치 / 5,000원 + +- 수익률은 소수점 둘째자리에서 반올림 한다. + +6. 유저는 예외 상황시 에러문구를 힌트로 받을 수 있다. + +- 에러 문구는 "[ERROR]"로 시작해야 한다 + +## 요구사항 + +indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. +예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. +힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. + +함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. +Jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다. + +함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다. + +함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다. + +도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(Console.readLineAsync, Console.print) 로직에 대한 단위 테스트는 제외한다. + +핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. + +라이브러리 사용해서 랜덤 및 출력 구현 + +## 제약사항 + +Lotto 클래스 +제공된 Lotto 클래스를 활용해 구현해야 한다. +numbers의 # prefix를 변경할 수 없다. +Lotto에 필드를 추가할 수 없다. diff --git a/docs/image.png b/docs/image.png new file mode 100644 index 0000000000..6384dae082 Binary files /dev/null and b/docs/image.png differ diff --git a/src/Aligner/Aligner.js b/src/Aligner/Aligner.js new file mode 100644 index 0000000000..8a2a77690f --- /dev/null +++ b/src/Aligner/Aligner.js @@ -0,0 +1,7 @@ +const Aligner = class { + static alignAscendingArr(arr) { + arr.sort((a, b) => a - b); + } +}; + +export default Aligner; diff --git a/src/App.js b/src/App.js index c38b30d5b2..65f7320744 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,50 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import Converter from './Converter/Converter.js'; +import Lotto from './Lotto/Lotto.js'; +import LottoIntergrator from './Lotto/LottoIntergrator.js'; +import Receiver from './Receiver/Receiver.js'; + +import Validator from './Validator/Validator.js'; +import Printer from './Printer/Printer.js'; + class App { - async play() {} + #userMoney; + + #userSetLottoNums; + + #bonusNum; + + #lottos = new LottoIntergrator(); + + async play() { + this.#userMoney = await Receiver.receiveMoney(); + + Validator.checkPurchaseAmount(this.#userMoney); + + this.#userMoney /= 1000; + + this.generateLottos(); + + Printer.printPublishInfo(this.#userMoney, this.#lottos.allLottoInfo()); + + this.#userSetLottoNums = Converter.stringToArray(await Receiver.receiveLottomNums()); + + Validator.checkLottoNums(this.#userSetLottoNums); + + this.#bonusNum = await Receiver.receiveBonusNum(); + + Validator.checkBonusNum(this.#bonusNum); + + console.log(this.#lottos.saveBoard(this.#userSetLottoNums, this.#bonusNum)); + } + + generateLottos() { + for (let i = 0; i < this.#userMoney; i += 1) { + const lotto = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6); + + this.#lottos.pushLotto(new Lotto(lotto).getLotto()); + } + } } export default App; diff --git a/src/Constant/Constant.js b/src/Constant/Constant.js new file mode 100644 index 0000000000..d0cb1d2190 --- /dev/null +++ b/src/Constant/Constant.js @@ -0,0 +1,27 @@ +const ERROR_MEESAGE = Object.freeze({ + NOT_VALID_MONEY: '1000원 단위로 입력해 주세요', + NOT_MINUS_NUMBER: '양의 정수를 입력해주세요', + ISNAN_ERROR_MSG: '숫자만 입력해주세요', + RANGE_ERROR_MSG: '1에서 45사이의 수만 입력해주세요', + NOT_VALID_SIZE: '6개의 로또를 입력해주세요', + NOT_DUPLICATE: '중복되지 않은 수를 입력해주세요', +}); + +const WINNING_AMOUNT_UNITS = Object.freeze({ + FIRST_PLACE: 2000000000, + SECOND_PLACE: 30000000, + THIRD_PLACE: 1500000, + FOURTH_PLACE: 50000, + FIFTH_PLACE: 5000, +}); + +const MONEY_UNIT = 1000; + +const RANGE_START_NUM = 1; +const RANGE_END_NUM = 45; +const LOTTIO_SIZE = 6; + +// prettier-ignore +export { + ERROR_MEESAGE, MONEY_UNIT, RANGE_START_NUM, RANGE_END_NUM, LOTTIO_SIZE, WINNING_AMOUNT_UNITS, +}; diff --git a/src/Converter/Converter.js b/src/Converter/Converter.js new file mode 100644 index 0000000000..f115ba26ee --- /dev/null +++ b/src/Converter/Converter.js @@ -0,0 +1,11 @@ +const Converter = class { + static stringToArray(string) { + return string.split(','); + } + + static arrToSetStructure(arr) { + return new Set(arr); + } +}; + +export default Converter; diff --git a/src/CustomError/CustonError.js b/src/CustomError/CustonError.js new file mode 100644 index 0000000000..12c5f8383b --- /dev/null +++ b/src/CustomError/CustonError.js @@ -0,0 +1,7 @@ +const CustomError = class { + constructor(errorMsg) { + throw new Error(`[ERROR] ${errorMsg}`); + } +}; + +export default CustomError; 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/Lotto/Lotto.js b/src/Lotto/Lotto.js new file mode 100644 index 0000000000..bfa90e091f --- /dev/null +++ b/src/Lotto/Lotto.js @@ -0,0 +1,22 @@ +import Aligner from '../Aligner/Aligner.js'; +import Validator from '../Validator/Validator.js'; + +class Lotto { + #numbers; + + constructor(numbers) { + Validator.checkLottoNums(numbers); + this.#numbers = numbers; + this.#align(this.#numbers); + } + + #align() { + Aligner.alignAscendingArr(this.#numbers); + } + + getLotto() { + return this.#numbers; + } +} + +export default Lotto; diff --git a/src/Lotto/LottoAnalyzer.js b/src/Lotto/LottoAnalyzer.js new file mode 100644 index 0000000000..4af043ad7a --- /dev/null +++ b/src/Lotto/LottoAnalyzer.js @@ -0,0 +1,22 @@ +export const LottoAnalyzer = class { + static getAnalysis(winningNumbers, bonusNumber, lottoNums) { + const matchCount = lottoNums.filter((num) => winningNumbers.includes(num)).length; + const isBonusMatched = lottoNums.includes(bonusNumber); + return LottoAnalyzer.getRank(matchCount, isBonusMatched); + } + + static getRank(matchCount, isBonusMatched) { + if (matchCount < 3) return 0; + if (matchCount === 3) return 5; + if (matchCount === 4) return 4; + if (matchCount === 5) return isBonusMatched ? 2 : 3; + if (matchCount === 6) return 1; + return 0; + } + + static toFixedNumber(number) { + return +number.toFixed(1); + } +}; + +export default LottoAnalyzer; diff --git a/src/Lotto/LottoIntergrator.js b/src/Lotto/LottoIntergrator.js new file mode 100644 index 0000000000..0ca380ea00 --- /dev/null +++ b/src/Lotto/LottoIntergrator.js @@ -0,0 +1,52 @@ +import { WINNING_AMOUNT_UNITS } from '../Constant/Constant.js'; + +import LottoAnalyzer from './LottoAnalyzer.js'; + +const LottoIntergrator = class { + #allLottos = []; + + #roi; + + #board = { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 0: 0, + }; + + pushLotto(lotto) { + this.#allLottos.push(lotto); + } + + allLottoInfo() { + return this.#allLottos; + } + + saveBoard(winNum, bonusNum) { + this.#allLottos.forEach((lottos) => { + this.#board[LottoAnalyzer.getAnalysis(winNum, bonusNum, lottos)] += 1; + }); + + return this.#board; + } + + static getstatistics(totalWinning, purchaseAmount) { + return LottoAnalyzer.toFixedNumber((totalWinning / purchaseAmount) * 0.1); + } + + static totalWinningAmount(winningArray) { + let totalAmount = 0; + winningArray.forEach((winning) => { + if (winning === 1) totalAmount += WINNING_AMOUNT_UNITS.FIRST_PLACE; + if (winning === 2) totalAmount += WINNING_AMOUNT_UNITS.SECOND_PLACEs; + if (winning === 3) totalAmount += WINNING_AMOUNT_UNITS.THIRD_PLACE; + if (winning === 4) totalAmount += WINNING_AMOUNT_UNITS.FOURTH_PLACE; + if (winning === 5) totalAmount += WINNING_AMOUNT_UNITS.FIFTH_PLACE; + }); + return totalAmount; + } +}; + +export default LottoIntergrator; diff --git a/src/Measurer/SizeMeasurer.js b/src/Measurer/SizeMeasurer.js new file mode 100644 index 0000000000..789391811f --- /dev/null +++ b/src/Measurer/SizeMeasurer.js @@ -0,0 +1,11 @@ +const SizeMeasurer = class { + static getArrSize(value) { + return value.length; + } + + static getSetStructureSize(value) { + return value.size; + } +}; + +export default SizeMeasurer; diff --git a/src/Printer/Printer.js b/src/Printer/Printer.js new file mode 100644 index 0000000000..cbdb30a3d3 --- /dev/null +++ b/src/Printer/Printer.js @@ -0,0 +1,16 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; + +export default class Printer { + static printPublishInfo(quantity, lottos) { + Printer.printPublishQuantity(quantity); + Printer.printPublishLottos(lottos); + } + + static printPublishLottos(lottos) { + lottos.forEach((lotto) => MissionUtils.Console.print(`[${lotto.join(',')}]`)); + } + + static printPublishQuantity(quantity) { + MissionUtils.Console.print(`${quantity}개를 구매했습니다.`); + } +} diff --git a/src/Receiver/Receiver.js b/src/Receiver/Receiver.js new file mode 100644 index 0000000000..de2affa800 --- /dev/null +++ b/src/Receiver/Receiver.js @@ -0,0 +1,35 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import { + LOTTIO_SIZE, + MONEY_UNIT, + RANGE_END_NUM, + RANGE_START_NUM, +} from '../Constant/Constant.js'; + +const Receiver = class { + static async receiveMoney() { + const answer = await MissionUtils.Console.readLineAsync( + `구입금액을 ${MONEY_UNIT}원 단위로 입력해 주세요.`, + ); + + return answer; + } + + static async receiveLottomNums() { + const answer = await MissionUtils.Console.readLineAsync( + `${RANGE_START_NUM}~${RANGE_END_NUM} 사이의 당첨 번호 ${LOTTIO_SIZE}개를 입력해 주세요(입력 시','로 구분해주세요).`, + ); + + return answer; + } + + static async receiveBonusNum() { + const answer = await MissionUtils.Console.readLineAsync( + `${RANGE_START_NUM}~${RANGE_END_NUM} 사이의 보너스 숫자를 입력해주세요`, + ); + + return answer; + } +}; + +export default Receiver; diff --git a/src/Sanitizer/Sanitizer.js b/src/Sanitizer/Sanitizer.js new file mode 100644 index 0000000000..fcf01bcb2c --- /dev/null +++ b/src/Sanitizer/Sanitizer.js @@ -0,0 +1,7 @@ +const Sanitizer = class { + static sanitizeEmpty(value) { + value.trim(); + } +}; + +export default Sanitizer; diff --git a/src/Validator/Validator.js b/src/Validator/Validator.js new file mode 100644 index 0000000000..8840ff54ae --- /dev/null +++ b/src/Validator/Validator.js @@ -0,0 +1,83 @@ +import { + ERROR_MEESAGE, + LOTTIO_SIZE, + MONEY_UNIT, + RANGE_END_NUM, + RANGE_START_NUM, +} from '../Constant/Constant.js'; +import Converter from '../Converter/Converter.js'; + +import CustomError from '../CustomError/CustonError.js'; +import SizeMeasurer from '../Measurer/SizeMeasurer.js'; + +const Validator = class { + static checkPurchaseAmount(money) { + Validator.checkIsNaN(money); + Validator.checkPositiveInt(money); + Validator.checkMoneyUnit(money); + } + + static checkLottoNums(lottoArr) { + lottoArr.forEach((lotto) => { + Validator.checkNumRange(lotto); + Validator.checkIsNaN(lotto); + }); + Validator.checkDuplicate(lottoArr); + Validator.checkLength(lottoArr); + } + + static checkBonusNum(number) { + Validator.checkIsNaN(+number); + Validator.checkNumRange(+number); + } + + static checkMoneyUnit(money) { + if (+money % MONEY_UNIT !== 0) { + throw new CustomError(ERROR_MEESAGE.NOT_VALID_MONEY); + } + + return true; + } + + static checkPositiveInt(number) { + if (Number.isInteger(+number) && +number < 0) { + throw new CustomError(ERROR_MEESAGE.NOT_MINUS_NUMBER); + } + + return true; + } + + static checkIsNaN(number) { + if (Number.isNaN(+number)) { + throw new CustomError(ERROR_MEESAGE.ISNAN_ERROR_MSG); + } + return true; + } + + static checkNumRange(number) { + if (+number < RANGE_START_NUM || +number > RANGE_END_NUM) { + throw new CustomError(ERROR_MEESAGE.RANGE_ERROR_MSG); + } + return true; + } + + static checkLength(arr) { + if (SizeMeasurer.getArrSize(arr) !== LOTTIO_SIZE) { + throw new CustomError(ERROR_MEESAGE.NOT_VALID_SIZE); + } + return true; + } + + static checkDuplicate(arr) { + // prettier-ignore + if ( + SizeMeasurer.getArrSize(Converter.arrToSetStructure(arr)) + !== SizeMeasurer.getSetStructureSize(arr) + ) { + throw new CustomError(ERROR_MEESAGE.NOT_DUPLICATE); + } + return true; + } +}; + +export default Validator; diff --git a/src/index.js b/src/index.js index 93eda3237a..0a3bc50015 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.play();