From c5dea8eb0b6537fefb0078f89311e08196256355 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:28:24 +0900 Subject: [PATCH 1/8] feat: add TypeScript definitions for mission-utils module --- types/woowacourse__mission-utils.d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 types/woowacourse__mission-utils.d.ts diff --git a/types/woowacourse__mission-utils.d.ts b/types/woowacourse__mission-utils.d.ts new file mode 100644 index 00000000..dc382dc8 --- /dev/null +++ b/types/woowacourse__mission-utils.d.ts @@ -0,0 +1,10 @@ +declare module "@woowacourse/mission-utils" { + export const Console: { + readLineAsync(prompt?: string): Promise; + print(message: string): void; + }; + + export const Random: { + pickNumberInRange(min: number, max: number): number; + }; +} From 69b903ed1ba7dfe1da2c4fe3a29dff231aebe2f0 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:28:57 +0900 Subject: [PATCH 2/8] feat: implement PlanetLotto class with validation logic --- src/domain/PlanetLotto.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/domain/PlanetLotto.js diff --git a/src/domain/PlanetLotto.js b/src/domain/PlanetLotto.js new file mode 100644 index 00000000..6f538b70 --- /dev/null +++ b/src/domain/PlanetLotto.js @@ -0,0 +1,38 @@ +import { LOTTO_RULE } from "../constants/config.js"; + +class PlanetLotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = [...numbers].sort((first, second) => first - second); + } + + #validate(numbers) { + if (!Array.isArray(numbers)) { + throw new Error("당첨 번호는 숫자여야 합니다."); + } + if (numbers.length !== LOTTO_RULE.COUNT) { + throw new Error("로또 번호는 5개여야 합니다."); + } + if (numbers.some((number) => !Number.isInteger(number))) { + throw new Error("당첨 번호는 숫자여야 합니다."); + } + if ( + numbers.some( + (number) => number < LOTTO_RULE.MIN || number > LOTTO_RULE.MAX + ) + ) { + throw new Error("로또 번호는 1부터 30 사이의 숫자여야 합니다."); + } + if (new Set(numbers).size !== numbers.length) { + throw new Error("로또 번호에 중복이 있습니다."); + } + } + + getNumbers() { + return [...this.#numbers]; + } +} + +export default PlanetLotto; From d082c62f0d100f1f81c5904a92e7475314f27e93 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:29:46 +0900 Subject: [PATCH 3/8] feat: add PlanetLottoGame class for managing lotto purchases and rank calculations --- src/domain/PlanetLottoGame.js | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/domain/PlanetLottoGame.js diff --git a/src/domain/PlanetLottoGame.js b/src/domain/PlanetLottoGame.js new file mode 100644 index 00000000..1191030a --- /dev/null +++ b/src/domain/PlanetLottoGame.js @@ -0,0 +1,65 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import { LOTTO_RULE, RANK } from "../constants/config.js"; +import PlanetLotto from "./PlanetLotto.js"; +import { + validatePurchaseAmount, + validateBonusNumber, +} from "../utils/validator.js"; + +export class PlanetLottoGame { + purchaseLottos(amount) { + validatePurchaseAmount(amount); + const purchaseCount = amount / 500; + + return Array.from({ length: purchaseCount }, () => this.#issueOneLotto()); + } + + #issueOneLotto() { + const numbers = MissionUtils.Random.pickUniqueNumbersInRange( + LOTTO_RULE.MIN, + LOTTO_RULE.MAX, + LOTTO_RULE.COUNT + ); + return new PlanetLotto(numbers).getNumbers(); + } + + calculateCountsByRank(purchasedLottos, winningNumbers, bonusNumber) { + const winningLotto = new PlanetLotto(winningNumbers); + validateBonusNumber(bonusNumber, winningLotto.getNumbers()); + + const countsByRank = this.#createCountsMap(); + purchasedLottos.forEach((lottoNumbers) => { + const rank = this.#determineRank( + lottoNumbers, + winningLotto.getNumbers(), + bonusNumber + ); + countsByRank.set(rank, (countsByRank.get(rank) ?? 0) + 1); + }); + + return countsByRank; + } + + #createCountsMap() { + const map = new Map(); + Object.values(RANK).forEach((rank) => map.set(rank, 0)); + return map; + } + + #determineRank(lottoNumbers, winningNumbers, bonusNumber) { + const matchCount = this.#countMatches(lottoNumbers, winningNumbers); + const hasBonus = lottoNumbers.includes(bonusNumber); + + if (matchCount === 5) return RANK.FIRST; + if (matchCount === 4 && hasBonus) return RANK.SECOND; + if (matchCount === 4) return RANK.THIRD; + if (matchCount === 3 && hasBonus) return RANK.FOURTH; + if (matchCount === 2 && hasBonus) return RANK.FIFTH; + return RANK.NONE; + } + + #countMatches(lottoNumbers, winningNumbers) { + const winningSet = new Set(winningNumbers); + return lottoNumbers.filter((number) => winningSet.has(number)).length; + } +} From 92d3bd10849e2e89520839a279a109998fb05b76 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:29:56 +0900 Subject: [PATCH 4/8] feat: add validation functions for purchase amount and bonus number in validator.js --- src/utils/validator.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 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 00000000..2ff89593 --- /dev/null +++ b/src/utils/validator.js @@ -0,0 +1,25 @@ +import { LOTTO_RULE, PRICE } from "../constants/config.js"; + +export const validatePurchaseAmount = (amount) => { + if (!Number.isInteger(amount)) { + throw new Error("구매금액은 숫자여야 합니다."); + } + if (amount <= 0) { + throw new Error("구매금액은 0보다 커야 합니다."); + } + if (amount % PRICE !== 0) { + throw new Error("구매금액은 500원 단위여야 합니다."); + } +}; + +export const validateBonusNumber = (bonusNumber, winningNumbers) => { + if (!Number.isInteger(bonusNumber)) { + throw new Error("보너스 번호는 숫자여야 합니다."); + } + if (bonusNumber < LOTTO_RULE.MIN || bonusNumber > LOTTO_RULE.MAX) { + throw new Error("로또 번호는 1부터 30 사이의 숫자여야 합니다."); + } + if (winningNumbers.includes(bonusNumber)) { + throw new Error("보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } +}; From bfec739dda25894e28f17b9e370d2da266bb5e31 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:30:52 +0900 Subject: [PATCH 5/8] feat: add configuration constants for lotto rules and pricing --- src/constants/config.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/constants/config.js diff --git a/src/constants/config.js b/src/constants/config.js new file mode 100644 index 00000000..6693be48 --- /dev/null +++ b/src/constants/config.js @@ -0,0 +1,16 @@ +export const LOTTO_RULE = Object.freeze({ + MIN: 1, + MAX: 30, + COUNT: 5, +}); + +export const PRICE = 500; + +export const RANK = Object.freeze({ + NONE: 0, + FIRST: 1, + SECOND: 2, + THIRD: 3, + FOURTH: 4, + FIFTH: 5, +}); From bcf383fa22156e082890f586c0cd4c298520c7bb Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:30:59 +0900 Subject: [PATCH 6/8] feat: add retryUntilSuccess utility function for handling asynchronous actions --- src/utils/retry.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/utils/retry.js diff --git a/src/utils/retry.js b/src/utils/retry.js new file mode 100644 index 00000000..149b34d3 --- /dev/null +++ b/src/utils/retry.js @@ -0,0 +1,9 @@ +export const retryUntilSuccess = async (action, onError) => { + while (true) { + try { + return await action(); + } catch (error) { + onError(error); + } + } +}; From 10941a539cfeecf3d7616d5451a08e0cfa0b1476 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:31:05 +0900 Subject: [PATCH 7/8] feat: implement main game logic in App class for purchasing lottos and calculating results --- src/App.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5..4c7ed575 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,36 @@ +import { InputView, OutputView } from "./view.js"; +import { PlanetLottoGame } from "./domain/PlanetLottoGame.js"; +import { retryUntilSuccess } from "./utils/retry.js"; class App { - async run() {} + async run() { + const game = new PlanetLottoGame(); + + const purchaseAmount = await retryUntilSuccess( + () => InputView.askAmount(), + (error) => OutputView.printErrorMessage(error.message) + ); + + const purchasedLottos = game.purchaseLottos(purchaseAmount); + OutputView.printPurchasedLottos(purchasedLottos); + + const winningNumbers = await retryUntilSuccess( + () => InputView.askWinningLotto(), + (error) => OutputView.printErrorMessage(error.message) + ); + + const bonusNumber = await retryUntilSuccess( + () => InputView.askBonusNumber(), + (error) => OutputView.printErrorMessage(error.message) + ); + + const countsByRank = game.calculateCountsByRank( + purchasedLottos, + winningNumbers, + bonusNumber + ); + + OutputView.printResult(countsByRank); + } } export default App; From d88e7d0202076b125a4a69c9989cd110dc49aafc Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sat, 10 Jan 2026 17:32:56 +0900 Subject: [PATCH 8/8] docs: update Readme --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index b168a180..6a028655 100644 --- a/README.md +++ b/README.md @@ -1 +1,28 @@ # javascript-planetlotto-precourse + +# 행성 로또 기능 목록 + +## 입력 + +- [x] 구입 금액 입력 받기 (숫자 검증) +- [x] 당첨 번호 입력 받기 (쉼표 구분 / 5개 / 중복 없음 / 1~30) +- [x] 보너스 번호 입력 받기 (1~30 / 당첨 번호와 중복 없음) +- [x] 입력 오류 시 `[ERROR]` 출력 후 해당 입력부터 재시도 + +## 로또 발행 + +- [x] 구입 금액 / 500 만큼 로또 발행 +- [x] `Random.pickUniqueNumbersInRange(1, 30, 5)` 사용 +- [x] 각 로또 번호 오름차순 정렬 출력 + +## 당첨 계산 + +- [x] 구매 로또와 당첨 번호 비교하여 등수별 카운트 집계 +- [x] 1~5등 기준과 상금 적용 + +## 출력 + +- [x] `n개를 구매했습니다.` 출력 +- [x] 각 로또 `[x, x, x, x, x]` 형태 출력 +- [x] 당첨 통계 헤더 및 구분선 출력 +- [x] 등수별 결과 라인 출력