From 9520db82d1643f8f0ec2dc4a58a74825f0a7ba49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 01:50:29 +0900 Subject: [PATCH 01/22] =?UTF-8?q?docs(readme):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EB=B6=84=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 15bb106b5..7d9feb290 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# javascript-lotto-precourse +## 기능 요구사항 분석 + +### 입력/출력 + +**입력** + +- [ ] 로또 구입 금액을 입력받는다. +- [ ] 당첨 번호와 보너스 번호를 입력받는다. +- [ ] 잘못된 값을 입력했을 경우 메시지와 함께 에러를 발생시킨 후 +해당 지점부터 다시 입력 받는다. + +**출력** + +- [ ] 발행할 로또 개수를 바탕으로 로또 수량 및 번호 리스트들을 출력한다. +- [ ] 당첨 내역을 출력한다. +- [ ] 수익률을 출력한다. +- [ ] 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다. + +
+ +### 기능 + +**로또 발행** + +- [ ] 로또 구입 금액만큼 발행할 로또 개수를 정한다. +- [ ] 로또 개수만큼 로또를 발행한다. +- [ ] 발행한 로또 번호를 오름차순으로 정렬한다. + +**결과 계산** + +- [ ] 발행된 로또와 당첨 번호를 비교하여 당첨 내역을 계산한다. +- [ ] 당첨 내역을 바탕으로 총 수익률을 계산한다. + + 수익률은 소수점 둘째 자리에서 반올림한다. + + +
+ + +### 에러 + +**입력값** + +- 로또 구입 금액 + - [ ] 구입 금액의 단위가 1,000으로 나누어 떨어지지 않을 경우 + - [ ] 숫자가 아닌 문자가 포함된 경우 +- 당첨 번호 + - [ ] 중복된 번호가 입력된 경우 + - [ ] 입력된 숫자가 6개가 아닌 경우 + - [ ] 구분자가 쉼표(,)가 아닌 경우 + - [ ] 숫자가 아닌 문자가 포함된 경우 +- 보너스 번호 + - [ ] 숫자가 아닌 문자가 포함된 경우 + +**로또 발행** + +- [ ] 중복된 번호가 발행된 경우 +- [ ] 발행된 번호가 6개가 아닌 경우 + +
+ +### 엣지 케이스 + +- [ ] 당첨되지 않았을 때 (2개 이하로 번호 일치) \ No newline at end of file From 30078836fa8b1796e4a6bef269d887461538ca90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 15:21:28 +0900 Subject: [PATCH 02/22] =?UTF-8?q?feat(input):=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EA=B0=92=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 올바른 입력값을 받을 때까지 반복해서 입력값을 요청하는 기능 포함 --- src/view/Input.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/view/Input.js diff --git a/src/view/Input.js b/src/view/Input.js new file mode 100644 index 000000000..bf34bb384 --- /dev/null +++ b/src/view/Input.js @@ -0,0 +1,29 @@ +import { Console } from '@woowacourse/mission-utils'; +import InputValidator from '../utils/Validator.js'; + +class Input { + /** + * 올바른 입력을 받을 때까지 반복해서 입력을 요청 + */ + static async readValidInput(key, message) { + try { + const input = await Console.readLineAsync(message); + InputValidator.runValidate(key, input); + + return input; + } catch (err) { + Console.print(err.message); + return Input.readValidInput(key, message); + } + } + + /** + * @param {string} key - 검증 키 (e.g. 'purchaseAmount', 'winningNumber') + * @param {string} message - 입력 요청 메시지 + */ + static readInputValues(key, message) { + return Input.readValidInput(key, message); + } +} + +export default Input; From 77751f117a15b101a0d9cbcf7990afbd0acce7cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 15:48:44 +0900 Subject: [PATCH 03/22] =?UTF-8?q?feat(validate):=20=ED=8C=8C=EC=82=AC?= =?UTF-8?q?=EB=93=9C=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=9E=85=EB=A0=A5=EA=B0=92=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 파사드 패턴을 활용하여InputValidator를 공통 입력값 검증 클래스로 추상화 - 구입 금액 검증 클래스 추가 --- src/utils/Validator.js | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/utils/Validator.js diff --git a/src/utils/Validator.js b/src/utils/Validator.js new file mode 100644 index 000000000..40e0deeaf --- /dev/null +++ b/src/utils/Validator.js @@ -0,0 +1,48 @@ +import { ERROR_MESSAGE, LOTTO_RULES, TERMS } from './constants.js'; + +/** + * 구입 금액 검증 + */ +class PurchaseAmountValidator { + static #validateIsNaN(value) { + if (Number.isNaN(Number(value))) { + throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE); + } + } + + static #validateIsValidUnit(value) { + if (value % LOTTO_RULES.TICKET_PRICE !== 0) { + throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT); + } + } + + static validate(value) { + this.#validateIsNaN(value); + this.#validateIsValidUnit(value); + } +} + +/** + * 입력값 검증기 + */ +class InputValidator { + static #validators = { + [TERMS.PURCHASE_AMOUNT]: (value) => PurchaseAmountValidator.validate(value), + }; + + // 공통 검증 로직 + static #commonValidate(input) { + if (input.trim().length === 0) { + throw new Error(ERROR_MESSAGE.BLANK_INPUT); + } + } + + static runValidate(key, value) { + InputValidator.#commonValidate(value); + + const validator = this.#validators[key]; + validator(value); + } +} + +export default InputValidator; From c0f7effa4e47cb5ddb9c093bed97f0a4039c726a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 15:49:49 +0900 Subject: [PATCH 04/22] =?UTF-8?q?docs(constant):=20=EC=A3=BC=EC=9A=94=20?= =?UTF-8?q?=EB=A7=A4=EC=A7=81=20=EC=8A=A4=ED=8A=B8=EB=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A7=A4=EC=A7=81=20=EB=84=98=EB=B2=84=20=EC=83=81=EC=88=98?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/constants.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/utils/constants.js diff --git a/src/utils/constants.js b/src/utils/constants.js new file mode 100644 index 000000000..2d3470e39 --- /dev/null +++ b/src/utils/constants.js @@ -0,0 +1,33 @@ +export const IO_MESSAGE = Object.freeze({ + PURCHASE_AMOUNT_INPUT: '구입금액을 입력해 주세요.', + WINNING_NUMBER_INPUT: '당첨 번호를 입력해 주세요.', + BONUS_NUMBER_INPUT: '보너스 번호를 입력해 주세요.', + + PURCHASE_COUNT_OUTPUT: (count) => `${count}개를 구입했습니다.`, + WINNING_STATISTICS_OUTPUT: '당첨 통계\n---', + TOTAL_PROFIT_OUTPUT: (profitRate) => + `총 수익률은 ${profitRate.toFixed(2)}%입니다.`, +}); + +export const ERROR_MESSAGE = Object.freeze({ + BLANK_INPUT: '[ERROR] 입력값이 비어 있습니다.', + + PURCHASE_AMOUNT_UNIT: '[ERROR] 구입 금액은 1,000원 단위여야 합니다.', + PURCHASE_AMOUNT_TYPE: '[ERROR] 구입 금액은 숫자여야 합니다.', +}); + +export const LOTTO_RULES = Object.freeze({ + TICKET_PRICE: 1000, + MIN_NUMBER: 1, + MAX_NUMBER: 45, + TICKET_NUMBER_COUNT: 6, +}); + +export const SEPERATOR = Object.freeze({ + COMMA: ',', +}); + +export const TERMS = Object.freeze({ + PURCHASE_AMOUNT: 'purchaseAmount', + WINNING_NUMBER: 'winningNumber', +}); From d9c0adc992c956930026ad086c99b520873ac194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 16:01:33 +0900 Subject: [PATCH 05/22] =?UTF-8?q?test(input):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EA=B5=AC=EC=9E=85=20=EA=B8=88=EC=95=A1=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/InputValidatorTest.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 __tests__/InputValidatorTest.js diff --git a/__tests__/InputValidatorTest.js b/__tests__/InputValidatorTest.js new file mode 100644 index 000000000..0ace195ef --- /dev/null +++ b/__tests__/InputValidatorTest.js @@ -0,0 +1,22 @@ +import { ERROR_MESSAGE, TERMS } from '../src/utils/constants'; +import InputValidator from '../src/utils/Validator'; + +describe('입력값 검증 테스트 (로또 구입 금액)', () => { + it.each([ + ['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT], + [ + '숫자가 아닌 문자가 포함된 경우', + 'lotto', + ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE, + ], + [ + '1,000원으로 나누어 떨어지지 않는 경우', + '900', + ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT, + ], + ])('%s', (_, input, expectedError) => { + expect(() => + InputValidator.runValidate(TERMS.PURCHASE_AMOUNT, input) + ).toThrow(expectedError); + }); +}); From da9aee784e56943fb6294d953f05f894bbae036b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 17:50:47 +0900 Subject: [PATCH 06/22] =?UTF-8?q?feat(parse):=20=ED=8C=8C=EC=84=9C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=B0=8F=20=EA=B5=AC=EC=9E=85=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=20=ED=8B=B0=EC=BC=93=20=EC=88=98=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/Parser.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/utils/Parser.js diff --git a/src/utils/Parser.js b/src/utils/Parser.js new file mode 100644 index 000000000..cd1aa5b90 --- /dev/null +++ b/src/utils/Parser.js @@ -0,0 +1,13 @@ +import { LOTTO_RULES } from './constants.js'; + +class Parser { + /** + * 구입 금액으로 구입가능한 로또 티켓 수 계산 + * @param {string} purchaseAmount - 구입 금액 문자열 (e.g. '5000') + */ + static getPurchaseCount(purchaseAmount) { + return Number(purchaseAmount) / LOTTO_RULES.TICKET_PRICE; + } +} + +export default Parser; From 438809ba01b9f52183e1cb06d20f34e453cf3ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 17:51:32 +0900 Subject: [PATCH 07/22] =?UTF-8?q?feat(lotto):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=B0=9C=ED=96=89=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/LottoMachine.js | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/model/LottoMachine.js diff --git a/src/model/LottoMachine.js b/src/model/LottoMachine.js new file mode 100644 index 000000000..2ad6a628c --- /dev/null +++ b/src/model/LottoMachine.js @@ -0,0 +1,49 @@ +import { Console, MissionUtils } from '@woowacourse/mission-utils'; +import { IO_MESSAGE, LOTTO_RULES } from '../utils/constants.js'; +import Parser from '../utils/Parser.js'; +import Lotto from './Lotto.js'; + +class LottoMachine { + /** + * 구입 가능한 로또 티켓 수 계산 + */ + static #determineLottoNumber(purchaseAmount) { + return Parser.getPurchaseCount(purchaseAmount); + } + + /** + * 한 개의 로또 번호 생성 (e.g. [8, 21, 23, 41, 42, 43]) + */ + static #generateLottoNumbers() { + return MissionUtils.Random.pickUniqueNumbersInRange( + LOTTO_RULES.MIN_NUMBER, + LOTTO_RULES.MAX_NUMBER, + LOTTO_RULES.TICKET_NUMBER_COUNT + ); + } + + /** + * 로또 티켓 발행 + */ + static #issueLottoTickets(purchaseCount) { + const issuedLottos = new Map(); + + Array.from({ length: purchaseCount }).forEach(() => { + const lottoNumbers = this.#generateLottoNumbers(); + const lotto = new Lotto(lottoNumbers); + + issuedLottos.set(lottoNumbers, lotto); + }); + + return issuedLottos; + } + + static run(purchaseAmount) { + const purchaseCount = this.#determineLottoNumber(purchaseAmount); + Console.print(IO_MESSAGE.PURCHASE_COUNT_OUTPUT(purchaseCount)); + + return this.#issueLottoTickets(purchaseCount); + } +} + +export default LottoMachine; From 135c9b2b2180da76038a1bcc050e5f6f69a8f33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 17:57:22 +0900 Subject: [PATCH 08/22] =?UTF-8?q?refactor(lotto):=20Lotto=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Lotto.js | 18 ------------------ src/model/Lotto.js | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 18 deletions(-) delete mode 100644 src/Lotto.js create mode 100644 src/model/Lotto.js diff --git a/src/Lotto.js b/src/Lotto.js deleted file mode 100644 index cb0b1527e..000000000 --- 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 000000000..96ed77526 --- /dev/null +++ b/src/model/Lotto.js @@ -0,0 +1,25 @@ +import { ERROR_MESSAGE } from '../utils/constants.js'; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + if (numbers.length !== 6) { + throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_COUNT); + } + + if (new Set(numbers).size !== 6) { + throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_DUPLICATE); + } + } + + // TODO: 당첨 번호를 받아 몇개와 일치하는지 + // TODO: 보너스 번호와 일치하는지 +} + +export default Lotto; From 0d3758fcae435537a627402a3adaa45a705c4f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 19:10:00 +0900 Subject: [PATCH 09/22] =?UTF-8?q?feat(validate):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=A5=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파일의 주요한 기능이 입력값 검증이라는 것을 고려하여 파일명 구체화 Validator -> InputValidator --- src/utils/InputValidator.js | 98 +++++++++++++++++++++++++++++++++++++ src/utils/Validator.js | 48 ------------------ src/view/Input.js | 2 +- 3 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 src/utils/InputValidator.js delete mode 100644 src/utils/Validator.js diff --git a/src/utils/InputValidator.js b/src/utils/InputValidator.js new file mode 100644 index 000000000..cf3a9a4e3 --- /dev/null +++ b/src/utils/InputValidator.js @@ -0,0 +1,98 @@ +import { ERROR_MESSAGE, LOTTO_RULES, SEPERATOR, TERMS } from './constants.js'; +import Parser from './Parser.js'; + +/** + * 당첨 번호 검증 + */ +class WinningNumberValidator { + static #validators = [ + this.#validateHasNaN, + this.#validateDuplicate, + this.#validateLength, + this.#validateRange, + ]; + + /** + * 숫자가 아닌 문자가 포함된 경우, 구분자가 쉼표(,)가 아닌 경우 검증 + */ + static #validateHasNaN(value) { + if (value.some((number) => Number.isNaN(number))) { + throw new Error(ERROR_MESSAGE.WINNING_NUMBER_TYPE); + } + } + + static #validateLength(value) { + if (value.length !== LOTTO_RULES.TICKET_NUMBER_COUNT) { + throw new Error(ERROR_MESSAGE.WINNING_NUMBER_LENGTH); + } + } + + static #validateDuplicate(value) { + if (new Set(value).size !== value.length) { + throw new Error(ERROR_MESSAGE.WINNING_NUMBER_DUPLICATE); + } + } + + static #validateRange(value) { + if ( + value.some((number) => number < LOTTO_RULES.MIN_NUMBER) || + value.some((number) => number > LOTTO_RULES.MAX_NUMBER) + ) { + throw new Error(ERROR_MESSAGE.WINNING_NUMBER_RANGE); + } + } + + static validate(value) { + const parsedValue = Parser.convertToNumberArray(value, SEPERATOR.COMMA); + + this.#validators.forEach((validator) => validator.call(this, parsedValue)); + } +} + +/** + * 구입 금액 검증 + */ +class PurchaseAmountValidator { + static #validateIsNaN(value) { + if (Number.isNaN(Number(value))) { + throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE); + } + } + + static #validateIsValidUnit(value) { + if (value % LOTTO_RULES.TICKET_PRICE !== 0) { + throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT); + } + } + + static validate(value) { + this.#validateIsNaN(value); + this.#validateIsValidUnit(value); + } +} + +/** + * 입력값 검증기 + */ +class InputValidator { + static #validators = { + [TERMS.PURCHASE_AMOUNT]: (value) => PurchaseAmountValidator.validate(value), + [TERMS.WINNING_NUMBER]: (value) => WinningNumberValidator.validate(value), + }; + + // 공통 검증 로직 + static #commonValidate(input) { + if (input.trim().length === 0) { + throw new Error(ERROR_MESSAGE.BLANK_INPUT); + } + } + + static runValidate(key, value) { + InputValidator.#commonValidate(value); + + const validator = this.#validators[key]; + validator(value); + } +} + +export default InputValidator; diff --git a/src/utils/Validator.js b/src/utils/Validator.js deleted file mode 100644 index 40e0deeaf..000000000 --- a/src/utils/Validator.js +++ /dev/null @@ -1,48 +0,0 @@ -import { ERROR_MESSAGE, LOTTO_RULES, TERMS } from './constants.js'; - -/** - * 구입 금액 검증 - */ -class PurchaseAmountValidator { - static #validateIsNaN(value) { - if (Number.isNaN(Number(value))) { - throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE); - } - } - - static #validateIsValidUnit(value) { - if (value % LOTTO_RULES.TICKET_PRICE !== 0) { - throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT); - } - } - - static validate(value) { - this.#validateIsNaN(value); - this.#validateIsValidUnit(value); - } -} - -/** - * 입력값 검증기 - */ -class InputValidator { - static #validators = { - [TERMS.PURCHASE_AMOUNT]: (value) => PurchaseAmountValidator.validate(value), - }; - - // 공통 검증 로직 - static #commonValidate(input) { - if (input.trim().length === 0) { - throw new Error(ERROR_MESSAGE.BLANK_INPUT); - } - } - - static runValidate(key, value) { - InputValidator.#commonValidate(value); - - const validator = this.#validators[key]; - validator(value); - } -} - -export default InputValidator; diff --git a/src/view/Input.js b/src/view/Input.js index bf34bb384..998d67a1a 100644 --- a/src/view/Input.js +++ b/src/view/Input.js @@ -1,5 +1,5 @@ import { Console } from '@woowacourse/mission-utils'; -import InputValidator from '../utils/Validator.js'; +import InputValidator from '../utils/InputValidator.js'; class Input { /** From 8e29c27220c6ec1c9a129e46d323c8d08f4a616f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 19:10:38 +0900 Subject: [PATCH 10/22] =?UTF-8?q?fix(parse):=20convertToNumberArray=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EA=B0=80=20=ED=8C=8C=EC=8B=B1=20=EC=8B=9C?= =?UTF-8?q?=EC=97=90=20Number('')=20=EA=B0=92=EC=9D=84=200=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8A=94=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=EC=97=90=20=EB=8C=80=EC=9D=91=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/Parser.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/Parser.js b/src/utils/Parser.js index cd1aa5b90..cc5759eec 100644 --- a/src/utils/Parser.js +++ b/src/utils/Parser.js @@ -8,6 +8,22 @@ class Parser { static getPurchaseCount(purchaseAmount) { return Number(purchaseAmount) / LOTTO_RULES.TICKET_PRICE; } + + /** + * 문자열을 구분자로 분리하여 숫자 배열로 변환 + * @param {string} input - 입력 문자열 + * @param {string} seperator - 구분자 + * @returns + */ + static convertToNumberArray(input, seperator) { + return input.split(seperator).map((number) => { + const trimmed = number.trim(); + + if (trimmed === '') return NaN; + + return Number(trimmed); + }); + } } export default Parser; From 59fe72558bf7208e6fbdbc1eb00ab6358517345d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 19:11:04 +0900 Subject: [PATCH 11/22] =?UTF-8?q?test(input):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=A5=EA=B0=92=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/InputValidatorTest.js | 41 ++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/__tests__/InputValidatorTest.js b/__tests__/InputValidatorTest.js index 0ace195ef..96a1e2836 100644 --- a/__tests__/InputValidatorTest.js +++ b/__tests__/InputValidatorTest.js @@ -1,5 +1,5 @@ import { ERROR_MESSAGE, TERMS } from '../src/utils/constants'; -import InputValidator from '../src/utils/Validator'; +import InputValidator from '../src/utils/InputValidator'; describe('입력값 검증 테스트 (로또 구입 금액)', () => { it.each([ @@ -20,3 +20,42 @@ describe('입력값 검증 테스트 (로또 구입 금액)', () => { ).toThrow(expectedError); }); }); + +describe('입력값 검증 테스트 (당첨 번호)', () => { + it.each([ + [ + '구분자가 쉼표(,)가 아닌 경우', + '1,2;3,4,5,6', + ERROR_MESSAGE.WINNING_NUMBER_TYPE, + ], + [ + '구분자 형식이 잘못된 경우', + ',1,2,3,4,5,6', + ERROR_MESSAGE.WINNING_NUMBER_TYPE, + ], + [ + '숫자가 아닌 문자가 포함된 경우', + '1,2,h,4,5,6', + ERROR_MESSAGE.WINNING_NUMBER_TYPE, + ], + [ + '숫자가 6개가 아닌 경우', + '1,2,3,4,5', + ERROR_MESSAGE.WINNING_NUMBER_LENGTH, + ], + [ + '중복된 숫자가 포함된 경우', + '1,2,3,4,5,5', + ERROR_MESSAGE.WINNING_NUMBER_DUPLICATE, + ], + [ + '1~45 범위 밖의 숫자가 포함된 경우', + '1,2,3,4,5,50', + ERROR_MESSAGE.WINNING_NUMBER_RANGE, + ], + ])('%s', (_, input, expectedError) => { + expect(() => + InputValidator.runValidate(TERMS.WINNING_NUMBER, input) + ).toThrow(expectedError); + }); +}); From fde8f5a69d033d17b68a91552da14a749bfe4d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 19:15:11 +0900 Subject: [PATCH 12/22] =?UTF-8?q?refactor(validate):=20=EA=B5=AC=EC=9E=85?= =?UTF-8?q?=20=EA=B8=88=EC=95=A1=20=EA=B2=80=EC=A6=9D=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검증 함수를 정적 프로퍼티에 추가하여 validate 실행 시 각 검증함수를 순회하며 검증하도록 개선 --- src/utils/InputValidator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/InputValidator.js b/src/utils/InputValidator.js index cf3a9a4e3..6ad1859c0 100644 --- a/src/utils/InputValidator.js +++ b/src/utils/InputValidator.js @@ -53,6 +53,8 @@ class WinningNumberValidator { * 구입 금액 검증 */ class PurchaseAmountValidator { + static #validators = [this.#validateIsNaN, this.#validateIsValidUnit]; + static #validateIsNaN(value) { if (Number.isNaN(Number(value))) { throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE); @@ -66,8 +68,7 @@ class PurchaseAmountValidator { } static validate(value) { - this.#validateIsNaN(value); - this.#validateIsValidUnit(value); + this.#validators.forEach((validator) => validator.call(this, value)); } } From be434cad66fd4b6fc9630cc95814302ac117171d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 20:57:13 +0900 Subject: [PATCH 13/22] =?UTF-8?q?feat(input):=20=EB=B3=B4=EB=84=88?= =?UTF-8?q?=EC=8A=A4=20=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=A5=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보너스 번호 검증 클래스 추가 - 입력값 검증 관련 상수 추가 - 보너스 번호 테스트 코드 추가 --- __tests__/InputValidatorTest.js | 16 ++++++++++++ src/utils/InputValidator.js | 44 ++++++++++++++++++++++----------- src/utils/constants.js | 21 ++++++++++++---- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/__tests__/InputValidatorTest.js b/__tests__/InputValidatorTest.js index 96a1e2836..b4f0a32eb 100644 --- a/__tests__/InputValidatorTest.js +++ b/__tests__/InputValidatorTest.js @@ -23,6 +23,7 @@ describe('입력값 검증 테스트 (로또 구입 금액)', () => { describe('입력값 검증 테스트 (당첨 번호)', () => { it.each([ + ['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT], [ '구분자가 쉼표(,)가 아닌 경우', '1,2;3,4,5,6', @@ -59,3 +60,18 @@ describe('입력값 검증 테스트 (당첨 번호)', () => { ).toThrow(expectedError); }); }); + +describe('입력값 검증 테스트 (로또 구입 금액)', () => { + it.each([ + ['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT], + [ + '숫자가 아닌 문자가 포함된 경우', + 'lotto', + ERROR_MESSAGE.BONUS_NUMBER_TYPE, + ], + ])('%s', (_, input, expectedError) => { + expect(() => InputValidator.runValidate(TERMS.BONUS_NUMBER, input)).toThrow( + expectedError + ); + }); +}); diff --git a/src/utils/InputValidator.js b/src/utils/InputValidator.js index 6ad1859c0..034d4dae2 100644 --- a/src/utils/InputValidator.js +++ b/src/utils/InputValidator.js @@ -2,7 +2,30 @@ import { ERROR_MESSAGE, LOTTO_RULES, SEPERATOR, TERMS } from './constants.js'; import Parser from './Parser.js'; /** - * 당첨 번호 검증 + * 구입 금액 + */ +class PurchaseAmountValidator { + static #validators = [this.#validateIsNaN, this.#validateIsValidUnit]; + + static #validateIsNaN(value) { + if (Number.isNaN(Number(value))) { + throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE); + } + } + + static #validateIsValidUnit(value) { + if (value % LOTTO_RULES.TICKET_PRICE !== 0) { + throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT); + } + } + + static validate(value) { + this.#validators.forEach((validator) => validator.call(this, value)); + } +} + +/** + * 당첨 번호 */ class WinningNumberValidator { static #validators = [ @@ -50,35 +73,28 @@ class WinningNumberValidator { } /** - * 구입 금액 검증 + * 보너스 번호 */ -class PurchaseAmountValidator { - static #validators = [this.#validateIsNaN, this.#validateIsValidUnit]; - +class BonusNumberValidator { static #validateIsNaN(value) { if (Number.isNaN(Number(value))) { - throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE); - } - } - - static #validateIsValidUnit(value) { - if (value % LOTTO_RULES.TICKET_PRICE !== 0) { - throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT); + throw new Error(ERROR_MESSAGE.BONUS_NUMBER_TYPE); } } static validate(value) { - this.#validators.forEach((validator) => validator.call(this, value)); + this.#validateIsNaN(value); } } /** - * 입력값 검증기 + * 입력값 검증 */ class InputValidator { static #validators = { [TERMS.PURCHASE_AMOUNT]: (value) => PurchaseAmountValidator.validate(value), [TERMS.WINNING_NUMBER]: (value) => WinningNumberValidator.validate(value), + [TERMS.BONUS_NUMBER]: (value) => BonusNumberValidator.validate(value), }; // 공통 검증 로직 diff --git a/src/utils/constants.js b/src/utils/constants.js index 2d3470e39..767818477 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,10 +1,10 @@ export const IO_MESSAGE = Object.freeze({ - PURCHASE_AMOUNT_INPUT: '구입금액을 입력해 주세요.', - WINNING_NUMBER_INPUT: '당첨 번호를 입력해 주세요.', - BONUS_NUMBER_INPUT: '보너스 번호를 입력해 주세요.', + PURCHASE_AMOUNT_INPUT: '구입금액을 입력해 주세요.\n', + WINNING_NUMBER_INPUT: '\n당첨 번호를 입력해 주세요.\n', + BONUS_NUMBER_INPUT: '\n보너스 번호를 입력해 주세요.\n', - PURCHASE_COUNT_OUTPUT: (count) => `${count}개를 구입했습니다.`, - WINNING_STATISTICS_OUTPUT: '당첨 통계\n---', + PURCHASE_COUNT_OUTPUT: (count) => `\n${count}개를 구입했습니다.`, + WINNING_STATISTICS_OUTPUT: '당첨 통계\n---\n', TOTAL_PROFIT_OUTPUT: (profitRate) => `총 수익률은 ${profitRate.toFixed(2)}%입니다.`, }); @@ -14,6 +14,16 @@ export const ERROR_MESSAGE = Object.freeze({ PURCHASE_AMOUNT_UNIT: '[ERROR] 구입 금액은 1,000원 단위여야 합니다.', PURCHASE_AMOUNT_TYPE: '[ERROR] 구입 금액은 숫자여야 합니다.', + + GENERATED_LOTTO_COUNT: '[ERROR] 로또 번호는 6개여야 합니다.', + GENERATED_LOTTO_DUPLICATE: '[ERROR] 로또 번호에 중복된 숫자가 있습니다.', + + WINNING_NUMBER_LENGTH: '[ERROR] 당첨 번호는 6개여야 합니다.', + WINNING_NUMBER_TYPE: '[ERROR] 당첨 번호의 입력 형식이 올바르지 않습니다.', + WINNING_NUMBER_DUPLICATE: '[ERROR] 당첨 번호에 중복된 숫자가 있습니다.', + WINNING_NUMBER_RANGE: '[ERROR] 당첨 번호는 1부터 45 사이의 숫자여야 합니다.', + + BONUS_NUMBER_TYPE: '[ERROR] 보너스 번호는 숫자여야 합니다.', }); export const LOTTO_RULES = Object.freeze({ @@ -30,4 +40,5 @@ export const SEPERATOR = Object.freeze({ export const TERMS = Object.freeze({ PURCHASE_AMOUNT: 'purchaseAmount', WINNING_NUMBER: 'winningNumber', + BONUS_NUMBER: 'bonusNumber', }); From deb3e6bf374cd578a863c5875b4db8ae4fbb931f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 22:49:36 +0900 Subject: [PATCH 14/22] =?UTF-8?q?refactor(lotto):=20Lotto=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=9C=ED=96=89=EB=90=9C=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lotto 클래스 내부적으로 getNumbers 메서드를 통해 값을 반환할 수 있게 되어 로또 번호를 Map의 key로 갖게 되는 것이 불필요하다고 판단. 이에 따라 issuedLottos의 데이터 구조를 Map -> 리스트 형태로 변경 --- src/model/Lotto.js | 20 +++++++++++++++----- src/model/LottoMachine.js | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/model/Lotto.js b/src/model/Lotto.js index 96ed77526..56a4a7c90 100644 --- a/src/model/Lotto.js +++ b/src/model/Lotto.js @@ -1,4 +1,4 @@ -import { ERROR_MESSAGE } from '../utils/constants.js'; +import { ERROR_MESSAGE, LOTTO_RULES } from '../utils/constants.js'; class Lotto { #numbers; @@ -9,17 +9,27 @@ class Lotto { } #validate(numbers) { - if (numbers.length !== 6) { + if (numbers.length !== LOTTO_RULES.TICKET_NUMBER_COUNT) { throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_COUNT); } - if (new Set(numbers).size !== 6) { + if (new Set(numbers).size !== numbers.length) { throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_DUPLICATE); } } - // TODO: 당첨 번호를 받아 몇개와 일치하는지 - // TODO: 보너스 번호와 일치하는지 + getNumbers() { + return [...this.#numbers]; + } + + calculateMatchCount(winningNumber) { + return this.#numbers.filter((number) => winningNumber.includes(number)) + .length; + } + + hasBonusNumber(bonusNumber) { + return this.#numbers.includes(Number(bonusNumber)); + } } export default Lotto; diff --git a/src/model/LottoMachine.js b/src/model/LottoMachine.js index 2ad6a628c..7dbc4d869 100644 --- a/src/model/LottoMachine.js +++ b/src/model/LottoMachine.js @@ -26,13 +26,13 @@ class LottoMachine { * 로또 티켓 발행 */ static #issueLottoTickets(purchaseCount) { - const issuedLottos = new Map(); + const issuedLottos = []; Array.from({ length: purchaseCount }).forEach(() => { const lottoNumbers = this.#generateLottoNumbers(); const lotto = new Lotto(lottoNumbers); - issuedLottos.set(lottoNumbers, lotto); + issuedLottos.push(lotto); }); return issuedLottos; From 59b020af99ba7fd541ebdbf6a00ad46b8437ac2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 22:54:41 +0900 Subject: [PATCH 15/22] =?UTF-8?q?feat(calculate):=20=EB=9E=AD=ED=81=AC?= =?UTF-8?q?=EB=8B=B9=20=EB=8B=B9=EC=B2=A8=20=ED=9A=9F=EC=88=98=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/RankCalculator.js | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/model/RankCalculator.js diff --git a/src/model/RankCalculator.js b/src/model/RankCalculator.js new file mode 100644 index 000000000..49c8a5bd3 --- /dev/null +++ b/src/model/RankCalculator.js @@ -0,0 +1,41 @@ +import { LOTTO_RULES } from '../utils/constants.js'; + +class RankCalculator { + /** + * 일치 결과(일치 개수, 보너스 여부)를 토대로 순위 배열의 인덱스로 반환 + * @param {number} matchCount - 일치한 번호의 개수 + * @param {boolean} hasBonus - 보너스 번호 일치 여부 + * @returns {number | undefined} 통계 배열의 인덱스 (0: 1등, 1: 2등, ... 4: 5등). 꽝은 undefined. + */ + static #getRankIndex(matchCount, hasBonus) { + if (matchCount === 5) return hasBonus ? 1 : 2; // 1: 2등 인덱스, 2: 3등 인덱스 + + // 일치 개수 : 랭크 인덱스 + const RANK_INDEX_MAP = { + 6: 0, // 1등 + 4: 3, // 4등 + 3: 4, // 5등 + }; + + return RANK_INDEX_MAP[matchCount]; + } + + static calculate(issuedLottos, winningNumber, bonusNumber) { + // [1등, 2등, 3등, 4등, 5등] 순서의 당첨 통계 배열 + // (예: [0, 0, 1, 0, 1] => 3등 1개, 5등 1개) + const rankCounts = new Array(LOTTO_RULES.TOTAL_RANK_COUNT).fill(0); + + issuedLottos.forEach((lotto) => { + const matchCount = lotto.calculateMatchCount(winningNumber); + const hasBonus = lotto.hasBonusNumber(bonusNumber); + + const rankIndex = this.#getRankIndex(matchCount, hasBonus); + + if (rankIndex !== undefined) rankCounts[rankIndex] += 1; + }); + + return rankCounts; + } +} + +export default RankCalculator; From 4154dc9c56e5c74160fb921aa8be8699b3d21396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 22:58:24 +0900 Subject: [PATCH 16/22] =?UTF-8?q?feat(output):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=B6=9C=EB=A0=A5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/StatisticsView.js | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/view/StatisticsView.js diff --git a/src/view/StatisticsView.js b/src/view/StatisticsView.js new file mode 100644 index 000000000..0c2528f81 --- /dev/null +++ b/src/view/StatisticsView.js @@ -0,0 +1,61 @@ +import { Console } from '@woowacourse/mission-utils'; +import { IO_MESSAGE } from '../utils/constants.js'; + +const PRIZE_INFO = [ + { description: '6개 일치', prize: 2_000_000_000 }, + { description: '5개 일치, 보너스 볼 일치', prize: 30_000_000 }, + { description: '5개 일치', prize: 1_500_000 }, + { description: '4개 일치', prize: 50_000 }, + { description: '3개 일치', prize: 5_000 }, +]; + +class StatisticsView { + /** + * 당첨 통계 헤더를 출력합니다. + */ + static #printStatisticsHeader() { + Console.print(IO_MESSAGE.WINNING_STATISTICS_OUTPUT); + } + + /** + * 개별 당첨 내역을 출력 + */ + static #printRankDetails(rankCounts) { + // 5등(index 4)부터 1등(index 0) 순서로 출력하기 위해 역순으로 순회 + for (let i = PRIZE_INFO.length - 1; i >= 0; i--) { + const { description, prize } = PRIZE_INFO[i]; + const count = rankCounts[i]; + const prizeString = prize.toLocaleString('ko-KR'); + + Console.print(`${description} (${prizeString}원) - ${count}개`); + } + } + + /** + * 총 수익률을 계산하고 출력합니다. + */ + static #printProfitRate(rankCounts, purchaseAmount) { + // 총 상금 계산 + const totalProfit = rankCounts.reduce((sum, count, index) => { + return sum + count * PRIZE_INFO[index].prize; + }, 0); + + // 수익률 계산 ( (총상금 / 구매금액) * 100 ) + const profitRate = (totalProfit / purchaseAmount) * 100; + + Console.print(IO_MESSAGE.TOTAL_PROFIT_OUTPUT(profitRate)); + } + + /** + * 당첨 통계 전체 결과를 출력합니다. + * @param {number[]} rankCounts - [1등, 2등, 3등, 4등, 5등] 당첨 개수 배열 + * @param {number} purchaseAmount - 총 구매 금액 + */ + static printResult(rankCounts, purchaseAmount) { + this.#printStatisticsHeader(); + this.#printRankDetails(rankCounts); + this.#printProfitRate(rankCounts, purchaseAmount); + } +} + +export default StatisticsView; From cf7772bbf592a769d6e24c3cc306cbad71554513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 23:00:00 +0900 Subject: [PATCH 17/22] =?UTF-8?q?feat(constant):=20=EB=A7=A4=EC=A7=81=20?= =?UTF-8?q?=EB=84=98=EB=B2=84=20=EC=83=81=EC=88=98=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EA=B0=9C=ED=96=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/constants.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/constants.js b/src/utils/constants.js index 767818477..b38c11957 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -3,8 +3,8 @@ export const IO_MESSAGE = Object.freeze({ WINNING_NUMBER_INPUT: '\n당첨 번호를 입력해 주세요.\n', BONUS_NUMBER_INPUT: '\n보너스 번호를 입력해 주세요.\n', - PURCHASE_COUNT_OUTPUT: (count) => `\n${count}개를 구입했습니다.`, - WINNING_STATISTICS_OUTPUT: '당첨 통계\n---\n', + PURCHASE_COUNT_OUTPUT: (count) => `\n${count}개를 구매했습니다.`, + WINNING_STATISTICS_OUTPUT: '\n당첨 통계\n---\n', TOTAL_PROFIT_OUTPUT: (profitRate) => `총 수익률은 ${profitRate.toFixed(2)}%입니다.`, }); @@ -31,6 +31,7 @@ export const LOTTO_RULES = Object.freeze({ MIN_NUMBER: 1, MAX_NUMBER: 45, TICKET_NUMBER_COUNT: 6, + TOTAL_RANK_COUNT: 5, }); export const SEPERATOR = Object.freeze({ From 9f4d022aebfe496e88e4ecec364bbb41cffbec59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 23:00:33 +0900 Subject: [PATCH 18/22] =?UTF-8?q?feat(app):=20=EC=95=B1=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=8B=A4=ED=96=89=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5d..b9a1d9ab5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,47 @@ +import { Console } from '@woowacourse/mission-utils'; +import LottoMachine from './model/LottoMachine.js'; +import { IO_MESSAGE, SEPERATOR, TERMS } from './utils/constants.js'; +import Input from './view/Input.js'; +import Parser from './utils/Parser.js'; +import RankCalculator from './model/RankCalculator.js'; +import StatisticsView from './view/StatisticsView.js'; + class App { - async run() {} + async run() { + const purchaseAmount = await Input.readInputValues( + TERMS.PURCHASE_AMOUNT, + IO_MESSAGE.PURCHASE_AMOUNT_INPUT + ); + + const issuedLottos = LottoMachine.run(purchaseAmount); + issuedLottos.forEach((lotto) => { + const numbers = lotto.getNumbers(); + numbers.sort((a, b) => a - b); + Console.print(numbers); + }); + + const winningNumber = await Input.readInputValues( + TERMS.WINNING_NUMBER, + IO_MESSAGE.WINNING_NUMBER_INPUT + ); + const parsedWinningNumber = Parser.convertToNumberArray( + winningNumber, + SEPERATOR.COMMA + ); + + const bonusNumber = await Input.readInputValues( + TERMS.BONUS_NUMBER, + IO_MESSAGE.BONUS_NUMBER_INPUT + ); + + const rankCounts = RankCalculator.calculate( + issuedLottos, + parsedWinningNumber, + bonusNumber + ); + + StatisticsView.printResult(rankCounts, purchaseAmount); + } } export default App; From f52ed6875759b7a93b46c1c9365d80e96ac74b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 23:13:52 +0900 Subject: [PATCH 19/22] =?UTF-8?q?fix(test):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BC=80=EC=9D=B4=EC=8A=A4=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 2 +- src/utils/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.js b/src/App.js index b9a1d9ab5..5dd7d489c 100644 --- a/src/App.js +++ b/src/App.js @@ -17,7 +17,7 @@ class App { issuedLottos.forEach((lotto) => { const numbers = lotto.getNumbers(); numbers.sort((a, b) => a - b); - Console.print(numbers); + Console.print(`[${numbers.join(', ')}]`); }); const winningNumber = await Input.readInputValues( diff --git a/src/utils/constants.js b/src/utils/constants.js index b38c11957..a2b4e169e 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -6,7 +6,7 @@ export const IO_MESSAGE = Object.freeze({ PURCHASE_COUNT_OUTPUT: (count) => `\n${count}개를 구매했습니다.`, WINNING_STATISTICS_OUTPUT: '\n당첨 통계\n---\n', TOTAL_PROFIT_OUTPUT: (profitRate) => - `총 수익률은 ${profitRate.toFixed(2)}%입니다.`, + `총 수익률은 ${profitRate.toFixed(1)}%입니다.`, }); export const ERROR_MESSAGE = Object.freeze({ From 1822d80034de869cd065b2b38cb8f6ea54394f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 23:28:20 +0900 Subject: [PATCH 20/22] =?UTF-8?q?docs(readme):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7d9feb290..2ae0bb34f 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,17 @@ **입력** -- [ ] 로또 구입 금액을 입력받는다. -- [ ] 당첨 번호와 보너스 번호를 입력받는다. -- [ ] 잘못된 값을 입력했을 경우 메시지와 함께 에러를 발생시킨 후 +- [x] 로또 구입 금액을 입력받는다. +- [x] 당첨 번호와 보너스 번호를 입력받는다. +- [x] 잘못된 값을 입력했을 경우 메시지와 함께 에러를 발생시킨 후 해당 지점부터 다시 입력 받는다. **출력** -- [ ] 발행할 로또 개수를 바탕으로 로또 수량 및 번호 리스트들을 출력한다. -- [ ] 당첨 내역을 출력한다. -- [ ] 수익률을 출력한다. -- [ ] 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다. +- [x] 발행할 로또 개수를 바탕으로 로또 수량 및 번호 리스트들을 출력한다. +- [x] 당첨 내역을 출력한다. +- [x] 수익률을 출력한다. +- [x] 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
@@ -22,43 +22,46 @@ **로또 발행** -- [ ] 로또 구입 금액만큼 발행할 로또 개수를 정한다. -- [ ] 로또 개수만큼 로또를 발행한다. -- [ ] 발행한 로또 번호를 오름차순으로 정렬한다. +- [x] 로또 구입 금액만큼 발행할 로또 개수를 정한다. +- [x] 로또 개수만큼 로또를 발행한다. +- [x] 발행한 로또 번호를 오름차순으로 정렬한다. **결과 계산** -- [ ] 발행된 로또와 당첨 번호를 비교하여 당첨 내역을 계산한다. -- [ ] 당첨 내역을 바탕으로 총 수익률을 계산한다. +- [x] 발행된 로또와 당첨 번호를 비교하여 당첨 내역을 계산한다. +- [x] 당첨 내역을 바탕으로 총 수익률을 계산한다. 수익률은 소수점 둘째 자리에서 반올림한다.
- ### 에러 **입력값** +- [x] 공백인 경우 - 로또 구입 금액 - - [ ] 구입 금액의 단위가 1,000으로 나누어 떨어지지 않을 경우 - - [ ] 숫자가 아닌 문자가 포함된 경우 + - [x] 구입 금액의 단위가 1,000으로 나누어 떨어지지 않을 경우 + - [x] 숫자가 아닌 문자가 포함된 경우 - 당첨 번호 - - [ ] 중복된 번호가 입력된 경우 - - [ ] 입력된 숫자가 6개가 아닌 경우 - - [ ] 구분자가 쉼표(,)가 아닌 경우 - - [ ] 숫자가 아닌 문자가 포함된 경우 + - [x] 구분자가 쉼표(,)가 아닌 경우 + - [x] 숫자가 아닌 문자가 포함된 경우 + - [x] 구분자 형식이 잘못된 경우 (e.g. ‘1,2,3,’ ‘1,2,,3’) + - [x] 구분자 사이에 공백이 포함된 경우 (e.g. ‘1, 2,3’ ‘1,2 ,3’) + - [x] 입력된 숫자가 6개가 아닌 경우 + - [x] 중복된 번호가 입력된 경우 + - [x] 1~45 범위의 숫자가 아닌 경우 - 보너스 번호 - - [ ] 숫자가 아닌 문자가 포함된 경우 + - [x] 숫자가 아닌 문자가 포함된 경우 **로또 발행** -- [ ] 중복된 번호가 발행된 경우 -- [ ] 발행된 번호가 6개가 아닌 경우 +- [x] 중복된 번호가 발행된 경우 +- [x] 발행된 번호가 6개가 아닌 경우
### 엣지 케이스 -- [ ] 당첨되지 않았을 때 (2개 이하로 번호 일치) \ No newline at end of file +- [x] 당첨되지 않았을 때 (2개 이하로 번호 일치) \ No newline at end of file From e641f4ec5c28b479e5cf988943e4d03b88c7d55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 23:38:55 +0900 Subject: [PATCH 21/22] =?UTF-8?q?refactor(app):=20App.run=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 App.run 메서드는 게임 실행의 모든 세부 단계를 모두 포함하고 있어 가독성이 낮고 단일 책임 원칙을 위배 이에 따라 각 단계를 명확한 책임을 가진 헬퍼 메서드로 분리 run 메서드는 전체적인 흐름만 보여주는 '컨트롤러' 역할을 하도록 구조 개선 --- src/App.js | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/App.js b/src/App.js index 5dd7d489c..03f4976be 100644 --- a/src/App.js +++ b/src/App.js @@ -7,19 +7,41 @@ import RankCalculator from './model/RankCalculator.js'; import StatisticsView from './view/StatisticsView.js'; class App { - async run() { - const purchaseAmount = await Input.readInputValues( + #issuedLottos = []; + + /** + * 1. 구입 금액을 입력받고 숫자로 변환 + */ + async getPurchaseAmount() { + const purchaseAmountStr = await Input.readInputValues( TERMS.PURCHASE_AMOUNT, IO_MESSAGE.PURCHASE_AMOUNT_INPUT ); + return Number(purchaseAmountStr); + } - const issuedLottos = LottoMachine.run(purchaseAmount); - issuedLottos.forEach((lotto) => { + /** + * 2. 로또 발행 + */ + issueLottos(purchaseAmount) { + this.#issuedLottos = LottoMachine.run(purchaseAmount); + } + + /** + * 3. 발행된 로또 번호를 정렬하여 출력 + */ + printIssuedLottos() { + this.#issuedLottos.forEach((lotto) => { const numbers = lotto.getNumbers(); numbers.sort((a, b) => a - b); Console.print(`[${numbers.join(', ')}]`); }); + } + /** + * 4. 당첨 번호와 보너스 번호를 입력받고 파싱 + */ + async getWinningNumbers() { const winningNumber = await Input.readInputValues( TERMS.WINNING_NUMBER, IO_MESSAGE.WINNING_NUMBER_INPUT @@ -34,9 +56,20 @@ class App { IO_MESSAGE.BONUS_NUMBER_INPUT ); + return { winningNumber: parsedWinningNumber, bonusNumber }; + } + + // 전체 프로세스 실행 + async run() { + const purchaseAmount = await this.getPurchaseAmount(); + this.issueLottos(purchaseAmount); + this.printIssuedLottos(); + + const { winningNumber, bonusNumber } = await this.getWinningNumbers(); + const rankCounts = RankCalculator.calculate( - issuedLottos, - parsedWinningNumber, + this.#issuedLottos, + winningNumber, bonusNumber ); From 7747febf90c7504969d4aa1e33053ccc764b1281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=88=EC=95=88?= Date: Mon, 3 Nov 2025 23:40:58 +0900 Subject: [PATCH 22/22] =?UTF-8?q?chore(test):=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20eslint=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=EC=97=90=20=EB=94=B0=EB=9D=BC=20single=20quo?= =?UTF-8?q?te=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/InputValidatorTest.js | 2 +- __tests__/LottoTest.js | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/__tests__/InputValidatorTest.js b/__tests__/InputValidatorTest.js index b4f0a32eb..6bda359cb 100644 --- a/__tests__/InputValidatorTest.js +++ b/__tests__/InputValidatorTest.js @@ -61,7 +61,7 @@ describe('입력값 검증 테스트 (당첨 번호)', () => { }); }); -describe('입력값 검증 테스트 (로또 구입 금액)', () => { +describe('입력값 검증 테스트 (보너스 번호)', () => { it.each([ ['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT], [ diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..9abfb89f7 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,18 +1,15 @@ -import Lotto from "../src/Lotto"; +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]'); }); - - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 });