From 1bd16c00d305ea1612deadca44d9b4e302732c0b Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 13:11:20 +0900 Subject: [PATCH 01/11] =?UTF-8?q?init:=20=EB=A9=94=EC=9D=B8=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/LottoController.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/controller/LottoController.js diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 00000000..1086eb09 --- /dev/null +++ b/src/controller/LottoController.js @@ -0,0 +1,7 @@ +class LottoController { + constructor() {} + + async controlLotto() {} +} + +export default LottoController; From 545c0e66c20953221ee6fd4fb9413d940b8d8732 Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 13:12:02 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20App.js=EC=99=80=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5..6b42acb5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import LottoController from "./controller/lottoController.js"; + class App { - async run() {} + async run() { + const lottoController = new LottoController(); + await lottoController.controlLotto(); + } } export default App; From ad071a41b8e090d2ff30212a536ae3afe6bf9e03 Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 13:21:32 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=EC=99=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=EB=90=9C=20=EC=83=81=EC=88=98=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/lotto.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/constants/lotto.js diff --git a/src/constants/lotto.js b/src/constants/lotto.js new file mode 100644 index 00000000..7f777941 --- /dev/null +++ b/src/constants/lotto.js @@ -0,0 +1,5 @@ +export const LOTTO_PRICE = 500; +export const LOTTO_MIN_NUMBER = 1; +export const LOTTO_MAX_NUMBER = 30; +export const LOTTO_SIZE = 5; +export const LOTTO_DELIMITER = ","; From a32abe6ebf689642001ae71f1d312d8f2e38ba4f Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 13:25:55 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EC=9E=85=EB=A0=A5=EA=B0=92=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B5=AC=EC=9E=85=20=EA=B8=88=EC=95=A1=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/message.js | 4 ++++ src/controller/LottoController.js | 24 ++++++++++++++++++++++-- src/service/LottoValidator.js | 12 ++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/constants/message.js create mode 100644 src/service/LottoValidator.js diff --git a/src/constants/message.js b/src/constants/message.js new file mode 100644 index 00000000..06f6ead9 --- /dev/null +++ b/src/constants/message.js @@ -0,0 +1,4 @@ +export const ERROR_MESSAGE = { + EMPTY_INPUT: "입력값이 비어 있습니다.", + INVALID_PURCHASE_MIN: "구입 금액은 500원 이상이어야 합니다.", +}; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 1086eb09..6d3a539c 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,7 +1,27 @@ +import { InputView, OutputView } from "../view.js"; + class LottoController { - constructor() {} + #inputView; + #outputView; + constructor() { + this.#inputView = InputView; + this.#outputView = OutputView; + } + + async controlLotto() { + const amount = await this.inputAmount(); + } - async controlLotto() {} + async inputAmount() { + while (true) { + try { + const amount = await this.#inputView.askAmount(); + return amount; + } catch (error) { + this.#outputView.printErrorMessage(error.message); + } + } + } } export default LottoController; diff --git a/src/service/LottoValidator.js b/src/service/LottoValidator.js new file mode 100644 index 00000000..c8763dac --- /dev/null +++ b/src/service/LottoValidator.js @@ -0,0 +1,12 @@ +import { LOTTO_PRICE } from "../constants/lotto.js"; +import { ERROR_MESSAGE } from "../constants/message.js"; + +class LottoValidator { + validateAmount(amount) { + if (amount < LOTTO_PRICE) { + throw new Error(ERROR_MESSAGE.INVALID_PURCHASE_MIN); + } + } +} + +export default LottoValidator; From a9197024558c4ad71b44523efc8d247af7a43a4f Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 13:42:03 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B0=92=20=EA=B2=80=EC=A6=9D=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/constants/message.js | 5 +++++ src/controller/LottoController.js | 30 +++++++++++++++++++++++++++ src/service/LottoValidator.js | 34 ++++++++++++++++++++++++++++++- src/utils/validator.js | 7 +++++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/utils/validator.js diff --git a/src/constants/message.js b/src/constants/message.js index 06f6ead9..d4f1dc0c 100644 --- a/src/constants/message.js +++ b/src/constants/message.js @@ -1,4 +1,9 @@ export const ERROR_MESSAGE = { EMPTY_INPUT: "입력값이 비어 있습니다.", INVALID_PURCHASE_MIN: "구입 금액은 500원 이상이어야 합니다.", + INVALID_LOTTO_NUMBER: "로또 번호는 1부터 30 사이의 정수여야 합니다.", + INVALID_LOTTO_SIZE: "로또 번호는 5개여야 합니다.", + DUPLICATED_NUMBER: "로또 번호는 중복된 숫자가 발생할 수 없습니다.", + DUPLICATED_LOTTO_AND_BONUS_NUMBER: + "로또 번호와 보너스 번호는 중복될 수 없습니다.", }; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 6d3a539c..06c59411 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,27 +1,57 @@ +import LottoValidator from "../service/LottoValidator.js"; import { InputView, OutputView } from "../view.js"; class LottoController { #inputView; #outputView; + #validator; constructor() { this.#inputView = InputView; this.#outputView = OutputView; + this.#validator = new LottoValidator(); } async controlLotto() { const amount = await this.inputAmount(); + const winningNumbers = await this.inputWinningNumbers(); + const bonusNumber = await this.inputBonusNumber(winningNumbers); } async inputAmount() { while (true) { try { const amount = await this.#inputView.askAmount(); + this.#validator.validateAmount(amount); return amount; } catch (error) { this.#outputView.printErrorMessage(error.message); } } } + + async inputWinningNumbers() { + while (true) { + try { + const winningNumbers = await this.#inputView.askWinningLotto(); + this.#validator.validateWinningNumbers(winningNumbers); + return winningNumbers; + } catch (error) { + this.#outputView.printErrorMessage(error.message); + } + } + } + + async inputBonusNumber(winningNumbers) { + while (true) { + try { + const bonusNumber = await this.#inputView.askBonusNumber(); + this.#validator.validateBonusNumber(winningNumbers, bonusNumber); + return bonusNumber; + } catch (error) { + this.#outputView.printErrorMessage(error.message); + } + } + } } export default LottoController; diff --git a/src/service/LottoValidator.js b/src/service/LottoValidator.js index c8763dac..2ff884a6 100644 --- a/src/service/LottoValidator.js +++ b/src/service/LottoValidator.js @@ -1,5 +1,11 @@ -import { LOTTO_PRICE } from "../constants/lotto.js"; +import { + LOTTO_MAX_NUMBER, + LOTTO_MIN_NUMBER, + LOTTO_PRICE, + LOTTO_SIZE, +} from "../constants/lotto.js"; import { ERROR_MESSAGE } from "../constants/message.js"; +import { hasNoDuplicates, isInRange } from "../utils/validator.js"; class LottoValidator { validateAmount(amount) { @@ -7,6 +13,32 @@ class LottoValidator { throw new Error(ERROR_MESSAGE.INVALID_PURCHASE_MIN); } } + + validateWinningNumbers(numbers) { + if (numbers.length !== LOTTO_SIZE) { + throw new Error(ERROR_MESSAGE.INVALID_LOTTO_SIZE); + } + + numbers.forEach((number) => this.#validateLottoNumbers(number)); + + if (!hasNoDuplicates(numbers)) { + throw new ERROR(ERROR_MESSAGE.DUPLICATED_NUMBER); + } + } + + validateBonusNumber(winningNumbers, bonusNumber) { + this.#validateLottoNumbers(bonusNumber); + + if (winningNumbers.includes(bonusNumber)) { + throw new Error(ERROR_MESSAGE.DUPLICATED_LOTTO_AND_BONUS_NUMBER); + } + } + + #validateLottoNumbers(number) { + if (!isInRange(number, LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER)) { + throw new Error(ERROR_MESSAGE.INVALID_LOTTO_NUMBER); + } + } } export default LottoValidator; diff --git a/src/utils/validator.js b/src/utils/validator.js new file mode 100644 index 00000000..201edd12 --- /dev/null +++ b/src/utils/validator.js @@ -0,0 +1,7 @@ +export function isInRange(value, min, max) { + return value >= min && value <= max; +} + +export function hasNoDuplicates(array) { + return new Set(array).size === array.length; +} From 98d52bcb545cf844f37626b911e2017363fd5868 Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 14:18:28 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=82=9C=EC=88=98=20=EC=83=9D=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20Lotto=20=EA=B0=9D=EC=B2=B4=EC=97=90=20=EC=82=BD=EC=9E=85?= =?UTF-8?q?=ED=95=98=EB=8A=94=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/constants/message.js | 2 +- src/controller/LottoController.js | 16 +++++++++++++ src/model/Lotto.js | 40 +++++++++++++++++++++++++++++++ src/service/LottoService.js | 39 ++++++++++++++++++++++++++++++ src/utils/randomGenerator.js | 8 +++++++ src/view.js | 38 +++++++++++++++++++---------- 6 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 src/model/Lotto.js create mode 100644 src/service/LottoService.js create mode 100644 src/utils/randomGenerator.js diff --git a/src/constants/message.js b/src/constants/message.js index d4f1dc0c..85b0e0d1 100644 --- a/src/constants/message.js +++ b/src/constants/message.js @@ -1,7 +1,7 @@ export const ERROR_MESSAGE = { EMPTY_INPUT: "입력값이 비어 있습니다.", INVALID_PURCHASE_MIN: "구입 금액은 500원 이상이어야 합니다.", - INVALID_LOTTO_NUMBER: "로또 번호는 1부터 30 사이의 정수여야 합니다.", + INVALID_LOTTO_NUMBER: "로또 번호는 1부터 30 사이의 숫자여야 합니다.", INVALID_LOTTO_SIZE: "로또 번호는 5개여야 합니다.", DUPLICATED_NUMBER: "로또 번호는 중복된 숫자가 발생할 수 없습니다.", DUPLICATED_LOTTO_AND_BONUS_NUMBER: diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 06c59411..f857a9f1 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,3 +1,4 @@ +import LottoService from "../service/LottoService.js"; import LottoValidator from "../service/LottoValidator.js"; import { InputView, OutputView } from "../view.js"; @@ -5,18 +6,33 @@ class LottoController { #inputView; #outputView; #validator; + #lottoService; constructor() { this.#inputView = InputView; this.#outputView = OutputView; this.#validator = new LottoValidator(); + this.#lottoService = new LottoService(); } async controlLotto() { const amount = await this.inputAmount(); + const { change, lottos } = this.#lottoService.publish(amount); + this.printPurchaseResult(amount, change, lottos); const winningNumbers = await this.inputWinningNumbers(); const bonusNumber = await this.inputBonusNumber(winningNumbers); } + printPurchaseResult(amount, change, lottos) { + const lottoArray = []; + for (let lotto of lottos) { + lottoArray.push(lotto.getNumbers()); + } + this.#outputView.printPurchasedLottos(lottoArray); + if (change !== 0) { + this.#outputView.printChangeMessage(amount, change); + } + } + async inputAmount() { while (true) { try { diff --git a/src/model/Lotto.js b/src/model/Lotto.js new file mode 100644 index 00000000..2030378a --- /dev/null +++ b/src/model/Lotto.js @@ -0,0 +1,40 @@ +import { LOTTO_SIZE } from "../constants/lotto.js"; +import { ERROR_MESSAGE } from "../constants/message.js"; +import { hasNoDuplicates } from "../utils/validator.js"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + if (numbers.length !== LOTTO_SIZE) { + throw new Error(ERROR_MESSAGE.INVALID_LOTTO_SIZE); + } + + if (!hasNoDuplicates(numbers)) { + throw new DefaultError(ERROR_MESSAGE.DUPLICATED_NUMBER); + } + } + + getNumbers() { + return [...this.#numbers]; + } + + matchCount(winningNumbers) { + const matchNumbers = this.#numbers.filter((number) => + winningNumbers.includes(number) + ); + + return matchNumbers.length; + } + + contains(number) { + return this.#numbers.includes(number); + } +} + +export default Lotto; diff --git a/src/service/LottoService.js b/src/service/LottoService.js new file mode 100644 index 00000000..0838fcde --- /dev/null +++ b/src/service/LottoService.js @@ -0,0 +1,39 @@ +import { Console } from "@woowacourse/mission-utils"; +import { + LOTTO_MAX_NUMBER, + LOTTO_MIN_NUMBER, + LOTTO_PRICE, + LOTTO_SIZE, +} from "../constants/lotto.js"; +import Lotto from "../model/Lotto.js"; +import { generateSortedRandomNubmers } from "../utils/randomGenerator.js"; + +class LottoService { + publish(amount) { + const { count, change } = this.#calculateLottoCount(amount); + const lottos = Array.from({ length: count }, () => { + return this.#createSingleLotto(); + }); + + return { change, lottos }; + } + + #calculateLottoCount(amount) { + return { + count: Math.floor(amount / LOTTO_PRICE), + change: Math.floor(amount % LOTTO_PRICE), + }; + } + + #createSingleLotto() { + const numbers = generateSortedRandomNubmers( + LOTTO_MIN_NUMBER, + LOTTO_MAX_NUMBER, + LOTTO_SIZE + ); + + return new Lotto(numbers); + } +} + +export default LottoService; diff --git a/src/utils/randomGenerator.js b/src/utils/randomGenerator.js new file mode 100644 index 00000000..47904fb4 --- /dev/null +++ b/src/utils/randomGenerator.js @@ -0,0 +1,8 @@ +import { Random } from "@woowacourse/mission-utils"; + +export function generateSortedRandomNubmers(minNumber, maxNumber, size) { + const numbers = Random.pickUniqueNumbersInRange(minNumber, maxNumber, size); + const sorted = numbers.sort((a, b) => a - b); + + return sorted; +} diff --git a/src/view.js b/src/view.js index ae6afd9c..72b3a447 100644 --- a/src/view.js +++ b/src/view.js @@ -5,10 +5,12 @@ const InputView = { * @returns {number} */ async askAmount() { - const input = await MissionUtils.Console.readLineAsync('구입금액을 입력해 주세요.\n'); + const input = await MissionUtils.Console.readLineAsync( + "구입금액을 입력해 주세요.\n" + ); const num = parseInt(input, 10); if (Number.isNaN(num)) { - throw new Error('구매금액은 숫자여야 합니다.'); + throw new Error("구매금액은 숫자여야 합니다."); } return num; }, @@ -17,15 +19,17 @@ const InputView = { * @returns {number[]} */ async askWinningLotto() { - const input = await MissionUtils.Console.readLineAsync('지난 주 당첨 번호를 입력해 주세요.\n'); + const input = await MissionUtils.Console.readLineAsync( + "지난 주 당첨 번호를 입력해 주세요.\n" + ); return input - .replaceAll(' ', '') - .split(',') + .replaceAll(" ", "") + .split(",") .map((s) => { const n = parseInt(s, 10); if (Number.isNaN(n)) { - throw new Error('당첨 번호는 숫자여야 합니다.'); + throw new Error("당첨 번호는 숫자여야 합니다."); } return n; }); @@ -35,10 +39,12 @@ const InputView = { * @returns {number} */ async askBonusNumber() { - const input = await MissionUtils.Console.readLineAsync('보너스 번호를 입력해 주세요.\n'); + const input = await MissionUtils.Console.readLineAsync( + "보너스 번호를 입력해 주세요.\n" + ); const num = parseInt(input, 10); if (Number.isNaN(num)) { - throw new Error('보너스 번호는 숫자여야 합니다.'); + throw new Error("보너스 번호는 숫자여야 합니다."); } return num; }, @@ -51,9 +57,9 @@ const OutputView = { printPurchasedLottos(lottos) { const lines = [ `${lottos.length}개를 구매했습니다.`, - ...lottos.map(lotto => `[${lotto.join(', ')}]`), + ...lottos.map((lotto) => `[${lotto.join(", ")}]`), ]; - MissionUtils.Console.print(lines.join('\n')); + MissionUtils.Console.print(lines.join("\n")); }, /** @@ -66,15 +72,15 @@ const OutputView = { const getCount = (k) => countsByRank.get(k) ?? 0; const output = [ - '당첨 통계', - '---', + "당첨 통계", + "---", `5개 일치 (100,000,000원) - ${getCount(1)}개`, `4개 일치, 보너스 번호 일치 (10,000,000원) - ${getCount(2)}개`, `4개 일치 (1,500,000원) - ${getCount(3)}개`, `3개 일치, 보너스 번호 일치 (500,000원) - ${getCount(4)}개`, `2개 일치, 보너스 번호 일치 (5,000원) - ${getCount(5)}개`, `0개 일치 (0원) - ${getCount(0)}개`, - ].join('\n'); + ].join("\n"); MissionUtils.Console.print(output); }, @@ -85,6 +91,12 @@ const OutputView = { printErrorMessage(message) { MissionUtils.Console.print(`[ERROR] ${message}`); }, + + printChangeMessage(amount, change) { + MissionUtils.Console.print( + `\n${amount}원 중 ${change}원을 거슬러 드리겠습니다.\n` + ); + }, }; export { InputView, OutputView }; From ab9b4d4c76513378ead78c5a0443d4060b2eb2f0 Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 14:38:00 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8,=20=EB=B3=B4=EB=84=88=EC=8A=A4=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EB=A5=BC=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8A=94=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=B0=8F=20=EB=A1=9C=EB=98=90=20=EB=93=B1?= =?UTF-8?q?=EC=88=98=20=EA=B7=9C=EC=B9=99=EC=9D=84=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=8F=84=EB=A9=94=EC=9D=B8=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/constants/lotto.js | 35 ++++++++++++++++++++++++++++++ src/controller/LottoController.js | 21 +++++++++++------- src/model/Rank.js | 36 +++++++++++++++++++++++++++++++ src/model/WinningLotto.js | 27 +++++++++++++++++++++++ 4 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 src/model/Rank.js create mode 100644 src/model/WinningLotto.js diff --git a/src/constants/lotto.js b/src/constants/lotto.js index 7f777941..68050594 100644 --- a/src/constants/lotto.js +++ b/src/constants/lotto.js @@ -3,3 +3,38 @@ export const LOTTO_MIN_NUMBER = 1; export const LOTTO_MAX_NUMBER = 30; export const LOTTO_SIZE = 5; export const LOTTO_DELIMITER = ","; +export const RANK_RULE = { + RANKS: [ + { + name: "FIRST", + matchCount: 5, + bonus: false, + prize: 100000000, + }, + { + name: "SECOND", + matchCount: 4, + bonus: true, + prize: 10000000, + }, + { + name: "THIRD", + matchCount: 4, + bonus: false, + prize: 1500000, + }, + { + name: "FOURTH", + matchCount: 3, + bonus: true, + prize: 500000, + }, + { + name: "FIFTH", + matchCount: 2, + bonus: true, + prize: 5000, + }, + ], + MISS: { name: "MISS", matchCount: 0, bonus: false, prize: 0 }, +}; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index f857a9f1..9b2d301e 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,3 +1,4 @@ +import WinningLotto from "../model/WinningLotto.js"; import LottoService from "../service/LottoService.js"; import LottoValidator from "../service/LottoValidator.js"; import { InputView, OutputView } from "../view.js"; @@ -15,14 +16,18 @@ class LottoController { } async controlLotto() { - const amount = await this.inputAmount(); + const amount = await this.#inputAmount(); + const { change, lottos } = this.#lottoService.publish(amount); - this.printPurchaseResult(amount, change, lottos); - const winningNumbers = await this.inputWinningNumbers(); - const bonusNumber = await this.inputBonusNumber(winningNumbers); + this.#printPurchaseResult(amount, change, lottos); + + const winningNumbers = await this.#inputWinningNumbers(); + const bonusNumber = await this.#inputBonusNumber(winningNumbers); + + const winningLotto = new WinningLotto(winningNumbers, bonusNumber); } - printPurchaseResult(amount, change, lottos) { + #printPurchaseResult(amount, change, lottos) { const lottoArray = []; for (let lotto of lottos) { lottoArray.push(lotto.getNumbers()); @@ -33,7 +38,7 @@ class LottoController { } } - async inputAmount() { + async #inputAmount() { while (true) { try { const amount = await this.#inputView.askAmount(); @@ -45,7 +50,7 @@ class LottoController { } } - async inputWinningNumbers() { + async #inputWinningNumbers() { while (true) { try { const winningNumbers = await this.#inputView.askWinningLotto(); @@ -57,7 +62,7 @@ class LottoController { } } - async inputBonusNumber(winningNumbers) { + async #inputBonusNumber(winningNumbers) { while (true) { try { const bonusNumber = await this.#inputView.askBonusNumber(); diff --git a/src/model/Rank.js b/src/model/Rank.js new file mode 100644 index 00000000..592a1b5d --- /dev/null +++ b/src/model/Rank.js @@ -0,0 +1,36 @@ +import { RANK_RULE } from "../constants/lotto"; + +class Rank { + #name; + #matchCount; + #bonus; + #prize; + constructor({ name, matchCount, bonus, prize }) { + this.#name = name; + this.#matchCount = matchCount; + this.#bonus = bonus; + this.#prize = prize; + } + + initializeRanks() { + const normalRanks = this.#createNormalRanks(); + + normalRanks.forEach((rank) => { + Rank[rank.#name] = rank; + }); + + Rank.MISS = this.#createMissRank(); + } + + #createNormalRanks() { + return RANK_RULE.RANKS.map((rule) => new Rank(rule)); + } + + #createMissRank() { + return new Rank(RANK_RULE.MISS); + } + + isMiss() { + return this.#name === "MISS"; + } +} diff --git a/src/model/WinningLotto.js b/src/model/WinningLotto.js new file mode 100644 index 00000000..d222ca50 --- /dev/null +++ b/src/model/WinningLotto.js @@ -0,0 +1,27 @@ +import { ERROR_MESSAGE } from "../constants/message.js"; + +class WinningLotto { + #winningNumbers; + #bonusNumber; + + constructor(winningNumbers, bonusNumber) { + this.#validate(winningNumbers, bonusNumber); + this.#winningNumbers = winningNumbers; + this.#bonusNumber = bonusNumber; + } + + #validate(winningNumbers, bonusNumber) { + if (winningNumbers.includes(bonusNumber)) { + throw new Error(ERROR_MESSAGE.DUPLICATED_LOTTO_AND_BONUS_NUMBER); + } + } + + match(lotto) { + const matchCount = lotto.matchCount(this.#winningNumbers); + const bonusMatch = lotto.contains(this.#bonusNumber); + + return { matchCount, bonusMatch }; + } +} + +export default WinningLotto; From 8e865145bf2a3aabe5b5d418e3664fcb5ca24fb7 Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 15:11:14 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EC=B5=9C?= =?UTF-8?q?=EC=A2=85=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EB=A1=9C=EB=98=90=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=EC=97=90=20=ED=8C=A9=ED=86=A0=EB=A6=AC=ED=8C=A8=ED=84=B4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/lotto.js | 7 ++++++- src/factory/RankFactory.js | 14 ++++++++++++++ src/model/LottoResult.js | 39 ++++++++++++++++++++++++++++++++++++++ src/model/Rank.js | 16 +++++----------- 4 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 src/factory/RankFactory.js create mode 100644 src/model/LottoResult.js diff --git a/src/constants/lotto.js b/src/constants/lotto.js index 68050594..23d3175a 100644 --- a/src/constants/lotto.js +++ b/src/constants/lotto.js @@ -35,6 +35,11 @@ export const RANK_RULE = { bonus: true, prize: 5000, }, + { + name: "MISS", + matchCount: 0, + bonus: false, + prize: 0, + }, ], - MISS: { name: "MISS", matchCount: 0, bonus: false, prize: 0 }, }; diff --git a/src/factory/RankFactory.js b/src/factory/RankFactory.js new file mode 100644 index 00000000..20be2da7 --- /dev/null +++ b/src/factory/RankFactory.js @@ -0,0 +1,14 @@ +import Rank from "../model/Rank.js"; + +class RankFactory { + static createRank(matchCount, bonusMatch) { + if (matchCount === 5) return Rank.FIRST; + if (matchCount === 4 && bonusMatch) return Rank.SECOND; + if (matchCount === 4) return Rank.THIRD; + if (matchCount === 3 && bonusMatch) return Rank.FOURTH; + if (matchCount === 2 && bonusMatch) return Rank.FIFTH; + if (matchCount === 0) return Rank.MISS; + } +} + +export default RankFactory; diff --git a/src/model/LottoResult.js b/src/model/LottoResult.js new file mode 100644 index 00000000..c70fd0c8 --- /dev/null +++ b/src/model/LottoResult.js @@ -0,0 +1,39 @@ +import { RANK_RULE } from "../constants/lotto"; +import RankFactory from "../factory/RankFactory"; + +class LottoResult { + #counts; + constructor() { + this.#counts = this.#initCounts(); + } + + #initCounts() { + const counts = new Map(); + RANK_RULE.RANKS.forEach((rankRule) => { + const rank = RankFactory.createRank(rankRule.matchCount, rankRule.bonus); + counts.set(rank, 0); + }); + + return counts; + } + + add(rank) { + this.#counts.set(rank, this.#counts.get(rank) + 1); + } + + getCountOf(rank) { + return this.#counts.get(rank) ?? 0; + } + + getTotalPrize() { + let sum = 0; + + for (const [rank, count] of this.#counts.entries()) { + sum += rank.prize * count; + } + + return sum; + } +} + +export default LottoResult; diff --git a/src/model/Rank.js b/src/model/Rank.js index 592a1b5d..16aae49c 100644 --- a/src/model/Rank.js +++ b/src/model/Rank.js @@ -12,25 +12,19 @@ class Rank { this.#prize = prize; } - initializeRanks() { + static initializeRanks() { const normalRanks = this.#createNormalRanks(); normalRanks.forEach((rank) => { Rank[rank.#name] = rank; }); - - Rank.MISS = this.#createMissRank(); } - #createNormalRanks() { + static #createNormalRanks() { return RANK_RULE.RANKS.map((rule) => new Rank(rule)); } +} - #createMissRank() { - return new Rank(RANK_RULE.MISS); - } +Rank.initializeRanks(); - isMiss() { - return this.#name === "MISS"; - } -} +export default Rank; From c67d3673efa8d2cb87fb3a61de5959a5cdf145de Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 15:49:42 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=EC=B5=9C=EC=A2=85=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=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/controller/LottoController.js | 2 + src/service/LottoService.js | 65 +++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 9b2d301e..aabd81ad 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -25,6 +25,8 @@ class LottoController { const bonusNumber = await this.#inputBonusNumber(winningNumbers); const winningLotto = new WinningLotto(winningNumbers, bonusNumber); + const result = this.#lottoService.fillMapResult(lottos, winningLotto); + this.#outputView.printResult(result); } #printPurchaseResult(amount, change, lottos) { diff --git a/src/service/LottoService.js b/src/service/LottoService.js index 0838fcde..162025f8 100644 --- a/src/service/LottoService.js +++ b/src/service/LottoService.js @@ -4,9 +4,12 @@ import { LOTTO_MIN_NUMBER, LOTTO_PRICE, LOTTO_SIZE, + RANK_RULE, } from "../constants/lotto.js"; import Lotto from "../model/Lotto.js"; import { generateSortedRandomNubmers } from "../utils/randomGenerator.js"; +import LottoResult from "../model/LottoResult.js"; +import RankFactory from "../factory/RankFactory.js"; class LottoService { publish(amount) { @@ -18,6 +21,68 @@ class LottoService { return { change, lottos }; } + #parseRankRule(rankRule) { + if (rankRule.name === "FIRST") { + return 1; + } + if (rankRule.name === "SECOND") { + return 2; + } + if (rankRule.name === "THIRD") { + return 3; + } + if (rankRule.name === "FOURTH") { + return 4; + } + if (rankRule.name === "FIFTH") { + return 5; + } + if (rankRule.name === "MISS") { + return 0; + } + } + + fillMapResult(lottos, winningLotto) { + const map = new Map(); + const result = this.fillLottoResult(lottos, winningLotto); + + RANK_RULE.RANKS.forEach((rankRule) => { + map.set( + this.#parseRankRule(rankRule), + result.getCountOf( + RankFactory.createRank(rankRule.matchCount, rankRule.bonus) + ) + ); + }); + + return map; + } + + fillLottoResult(lottos, winningLotto) { + const result = this.#createEmptyResult(); + + lottos.forEach((lotto) => { + const rank = this.#evaluateSingleLotto(lotto, winningLotto); + this.#accumulate(result, rank); + }); + + return result; + } + + #createEmptyResult() { + return new LottoResult(); + } + + #evaluateSingleLotto(lotto, winningLotto) { + const { matchCount, bonusMatch } = winningLotto.match(lotto); + + return RankFactory.createRank(matchCount, bonusMatch); + } + + #accumulate(result, rank) { + result.add(rank); + } + #calculateLottoCount(amount) { return { count: Math.floor(amount / LOTTO_PRICE), From d595ce4551b9a38414361af1308aa4eb3a922ed5 Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 16:34:02 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20import=20=EC=8B=9C=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=EC=9E=90=20=EB=AF=B8=EC=9E=91=EC=84=B1=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20npm=20start=EA=B0=80=20?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/LottoController.js | 8 ++++++-- src/model/LottoResult.js | 4 ++-- src/model/Rank.js | 2 +- src/service/LottoService.js | 11 +++++------ src/view.js | 8 +++++++- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index aabd81ad..32e05998 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -25,8 +25,12 @@ class LottoController { const bonusNumber = await this.#inputBonusNumber(winningNumbers); const winningLotto = new WinningLotto(winningNumbers, bonusNumber); - const result = this.#lottoService.fillMapResult(lottos, winningLotto); - this.#outputView.printResult(result); + const { resultMap } = this.#lottoService.fillMapResult( + lottos, + winningLotto + ); + + this.#outputView.printResult(resultMap); } #printPurchaseResult(amount, change, lottos) { diff --git a/src/model/LottoResult.js b/src/model/LottoResult.js index c70fd0c8..4e72f284 100644 --- a/src/model/LottoResult.js +++ b/src/model/LottoResult.js @@ -1,5 +1,5 @@ -import { RANK_RULE } from "../constants/lotto"; -import RankFactory from "../factory/RankFactory"; +import { RANK_RULE } from "../constants/lotto.js"; +import RankFactory from "../factory/RankFactory.js"; class LottoResult { #counts; diff --git a/src/model/Rank.js b/src/model/Rank.js index 16aae49c..763d0af2 100644 --- a/src/model/Rank.js +++ b/src/model/Rank.js @@ -1,4 +1,4 @@ -import { RANK_RULE } from "../constants/lotto"; +import { RANK_RULE } from "../constants/lotto.js"; class Rank { #name; diff --git a/src/service/LottoService.js b/src/service/LottoService.js index 162025f8..adce1649 100644 --- a/src/service/LottoService.js +++ b/src/service/LottoService.js @@ -1,4 +1,3 @@ -import { Console } from "@woowacourse/mission-utils"; import { LOTTO_MAX_NUMBER, LOTTO_MIN_NUMBER, @@ -43,19 +42,19 @@ class LottoService { } fillMapResult(lottos, winningLotto) { - const map = new Map(); - const result = this.fillLottoResult(lottos, winningLotto); + const resultMap = new Map(); + const lottoResult = this.fillLottoResult(lottos, winningLotto); RANK_RULE.RANKS.forEach((rankRule) => { - map.set( + resultMap.set( this.#parseRankRule(rankRule), - result.getCountOf( + lottoResult.getCountOf( RankFactory.createRank(rankRule.matchCount, rankRule.bonus) ) ); }); - return map; + return { resultMap, lottoResult }; } fillLottoResult(lottos, winningLotto) { diff --git a/src/view.js b/src/view.js index 72b3a447..6df91294 100644 --- a/src/view.js +++ b/src/view.js @@ -94,7 +94,13 @@ const OutputView = { printChangeMessage(amount, change) { MissionUtils.Console.print( - `\n${amount}원 중 ${change}원을 거슬러 드리겠습니다.\n` + `\n${amount.toLocaleString()}원 중 ${change.toLocaleString()}원을 거슬러 드리겠습니다.\n` + ); + }, + + printTotalProfit(profit) { + MissionUtils.Console.print( + `총 상금은 ${profit.toLocaleString()}원 입니다.` ); }, }; From 37bfb8aacf0d3db77b5d01122c9f50fa825d5cc5 Mon Sep 17 00:00:00 2001 From: ohsung0722 <99doldol@naver.com> Date: Sat, 10 Jan 2026 16:40:10 +0900 Subject: [PATCH 11/11] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=8A=A4=ED=8A=B8=20(=20=EC=88=98=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=EC=9E=91=EC=84=B1=ED=95=9C=20=EA=B2=83=20readme?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AE=EA=B8=B0=EA=B8=B0)=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/README.md b/README.md index b168a180..a12d6c46 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ # javascript-planetlotto-precourse + +## 기능 체크리스트 + +#### 로또 구입 + +- [x] 구입 금액 입력 +- [x] 구입 금액에 맞게 로또 개수 발행 +- [x] 500 단위로 발행하되, 거스름돈 계산 수행 + +#### 로또 발행 + +- [x] 한 장당 1 ~ 30 사이 중복되지 않는 5개 번호 랜덤 발행 +- [x] 번호 오름차순 정렬 +- [x] 구매 개수 및 로또 번호 목록 출력 + +#### 당첨 번호 및 보너스 입력 + +- [x] 당첨 번호를 쉼표를 구분하여 입력 +- [x] 입력된 번호는 5개 +- [x] 각 번호는 1 ~ 30 사이의 정수 +- [x] 당첨 번호 이외 보너스 번호 입력 + +#### 당첨 결과 계산 + +- [x] 구매한 모든 로또 번호와 당첨 번호 비교 +- [x] 일치 개수 + 보너스 번호 포함 여부 계산 +- [x] 일치 개수와 보너스 일치 여부 기준으로 등수 판별 +- [x] 등수 갯수 집계 +- [x] 총 상금 합산 + +#### 추가 기능 + +- [x] 거스름돈 계산 +- [] 총 상금을 기반으로 로또 교환 여부 작성 +- [] Y or N 로 입력 받은 후 N 입력 시 프로그램 종료 + +## 예외 체크리스트 + +#### 구입 금액 관련 예외 + +- [x] 구입 금액이 숫자가 아님 +- [x] 500원 미만으로 입력했을 때 + +#### 당첨 번호 입력 관련 예외 + +- [x] 쉼표로 구분되지 않은 입력 -> '1 2 3 4 5' +- [x] 숫자 외 문자 포함 +- [x] 숫자 5개 미만 +- [x] 숫자 5개 초과 +- [x] 중복된 번호 존재 +- [x] 숫자 범위 초과 (1 미만 or 30 초과) +- [x] 공백 또는 빈 입력 + +#### 보너스 번호 입력 관련 예외 + +- [x] 숫자가 아닌 입력 +- [x] 범위 초과 (1 미만 or 30 초과) +- [x] 당첨 번호와 중복 +- [x] 공백 또는 빈 입력