From 8f054645501da2607c55f39bae056abf54b931f3 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sun, 2 Nov 2025 19:43:43 +0900 Subject: [PATCH 01/20] =?UTF-8?q?chore(types):=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- types/woowacourse__mission-utils.d.ts | 6 ++++++ 1 file changed, 6 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 000000000..005746455 --- /dev/null +++ b/types/woowacourse__mission-utils.d.ts @@ -0,0 +1,6 @@ +declare module "@woowacourse/mission-utils" { + export const Console: { + readLineAsync(prompt?: string): Promise; + print(message: string): void; + }; +} From 80cf7e5327d0644dcf82feda9020e6c01162176b Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sun, 2 Nov 2025 20:34:50 +0900 Subject: [PATCH 02/20] =?UTF-8?q?docs(readme):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=AA=A9=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index 15bb106b5..b373d9b79 100644 --- a/README.md +++ b/README.md @@ -1 +1,66 @@ # javascript-lotto-precourse + +# ๐ŸŽฐ ๋กœ๋˜ + +## ๐Ÿ“‹ ๊ตฌํ˜„ ๊ธฐ๋Šฅ ๋ชฉ๋ก + +--- + +### 1๏ธโƒฃ ๋กœ๋˜ ๊ตฌ์ž… ๊ธˆ์•ก ์ž…๋ ฅ + +- [x] `Console.readLineAsync("๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")`๋กœ ๊ธˆ์•ก ์ž…๋ ฅ +- [x] 1,000์› ๋‹จ์œ„์ธ์ง€ ๊ฒ€์ฆ +- [x] 1,000์› ๋ฏธ๋งŒ ๋˜๋Š” 1,000์œผ๋กœ ๋‚˜๋ˆ„์–ด๋–จ์–ด์ง€์ง€ ์•Š์œผ๋ฉด `[ERROR]` ์ถœ๋ ฅ ํ›„ **ํ•ด๋‹น ์ž…๋ ฅ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ›๊ธฐ** + +--- + +### 2๏ธโƒฃ ๋กœ๋˜ ๋ฐœํ–‰ + +- [x] ๊ตฌ์ž… ๊ธˆ์•ก รท 1,000 = ๋ฐœํ–‰ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ +- [x] `MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6)` ์œผ๋กœ ๋กœ๋˜ ๋ฒˆํ˜ธ ์ƒ์„ฑ +- [x] 1๊ฐœ ๋กœ๋˜๋Š” ์ค‘๋ณต ์—†๋Š” 6๊ฐœ ๋ฒˆํ˜ธ +- [x] ๋ฒˆํ˜ธ๋Š” **์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ**ํ•ด์„œ ๋ณด๊ด€ +- [x] `N๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.` ์ถœ๋ ฅ ํ›„ ๊ฐ ๋กœ๋˜ ๋ฒˆํ˜ธ๋ฅผ `[1, 2, 3, 4, 5, 6]` ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅ + +--- + +### 3๏ธโƒฃ ๋‹น์ฒจ ๋ฒˆํ˜ธ ์ž…๋ ฅ + +- [x] `Console.readLineAsync("๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")` +- [x] ์‰ผํ‘œ(,) ๊ธฐ์ค€์œผ๋กœ 6๊ฐœ์ธ์ง€ ๊ฒ€์ฆ +- [x] ๊ฐ ์ˆซ์ž๊ฐ€ 1~45 ๋ฒ”์œ„์ธ์ง€ ๊ฒ€์ฆ +- [x] ์ค‘๋ณต ์ˆซ์ž ์—†๋„๋ก ๊ฒ€์ฆ +- [x] ์ž˜๋ชป๋˜๋ฉด `[ERROR] ...` ์ถœ๋ ฅ ํ›„ **๋‹น์ฒจ ๋ฒˆํ˜ธ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ›๊ธฐ** + +--- + +### 4๏ธโƒฃ ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ž…๋ ฅ + +- [x] `Console.readLineAsync("๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")` +- [x] 1~45 ๋ฒ”์œ„ ๊ฒ€์ฆ +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ **๊ฒน์น˜์ง€ ์•Š๋„๋ก** ๊ฒ€์ฆ +- [x] ์ž˜๋ชป๋˜๋ฉด `[ERROR] ...` ์ถœ๋ ฅ ํ›„ **๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ›๊ธฐ** + +--- + +### 5๏ธโƒฃ ๋‹น์ฒจ ํ†ต๊ณ„ ๊ณ„์‚ฐ + +- [x] ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ๋งคํ•œ ๋ชจ๋“  ๋กœ๋˜์™€ ๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ๋น„๊ต +- [x] ์ผ์น˜ ๊ฐœ์ˆ˜์— ๋”ฐ๋ผ 3๊ฐœ/4๊ฐœ/5๊ฐœ/5๊ฐœ+๋ณด๋„ˆ์Šค/6๊ฐœ ์ง‘๊ณ„ +- [x] ๋“ฑ์ˆ˜๋ณ„ ์ƒ๊ธˆ ํ•ฉ๊ณ„ ๊ณ„์‚ฐ + +--- + +### 6๏ธโƒฃ ์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ ๋ฐ ์ถœ๋ ฅ + +- [x] ์ด ์ƒ๊ธˆ รท ๊ตฌ์ž… ๊ธˆ์•ก ร— 100 +- [x] ์†Œ์ˆ˜์  ๋‘˜์งธ ์ž๋ฆฌ์—์„œ ๋ฐ˜์˜ฌ๋ฆผํ•ด **์†Œ์ˆ˜ ์ฒซ์งธ ์ž๋ฆฌ๊นŒ์ง€๋งŒ** ์ถœ๋ ฅ (์˜ˆ: `62.5%`) +- [x] `์ด ์ˆ˜์ต๋ฅ ์€ 62.5%์ž…๋‹ˆ๋‹ค.` ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅ + +--- + +### 7๏ธโƒฃ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์ž…๋ ฅ + +- [x] ์ž˜๋ชป๋œ ๊ฐ’ ์ž…๋ ฅ ์‹œ `[ERROR]`๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅ +- [x] **์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋˜**(throw), `App`์—์„œ ์žก์•„์„œ ํ•ด๋‹น ๋‹จ๊ณ„๋งŒ ๋‹ค์‹œ ์ž…๋ ฅ +- [x] `process.exit()` ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ From 1160ac436457747bcd0623f2b3be7e1479b23d31 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sun, 2 Nov 2025 20:35:28 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat(lotto):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B2=94=EC=9C=84=20=EB=B0=8F=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Lotto.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..5ba39b5c4 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -10,9 +10,18 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - } - // TODO: ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ + if ( + !numbers.every((num) => Number.isInteger(num) && num >= 1 && num <= 45) + ) { + throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + const uniqueCount = new Set(numbers).size; + if (uniqueCount !== 6) { + throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } } export default Lotto; From 8c2b3d3e0e6fee47498f420a5eec1c6b52c02086 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sun, 2 Nov 2025 20:54:09 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat(app):=20=EA=B5=AC=EC=9E=85=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=EC=9E=85=EB=A0=A5=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5d..25c52addb 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,36 @@ +import { Console } from "@woowacourse/mission-utils"; + +const LOTTO_PRICE = 1000; class App { - async run() {} + async run() { + const purchaseAmount = await this.#readPurchaseAmount(); + } + + async #readPurchaseAmount() { + while (true) { + const input = await Console.readLineAsync("๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); + try { + return this.#parsePurchaseAmount(input); + } catch (error) { + Console.print(error.message); + } + } + } + + #parsePurchaseAmount(input) { + const raw = (input ?? "").trim(); + if (!/^\d+$/.test(raw)) { + throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + const amount = Number(raw); + if (amount < LOTTO_PRICE) { + throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (amount % LOTTO_PRICE !== 0) { + throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ๋‹จ์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return amount; + } } export default App; From 87c9b9fff1865dfdeecbbc371bdfabce88e8a4c7 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sun, 2 Nov 2025 21:26:49 +0900 Subject: [PATCH 05/20] =?UTF-8?q?=20feat(app):=20=EA=B5=AC=EC=9E=85=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=A1=9C?= =?UTF-8?q?=EB=98=90=20=EB=B0=9C=ED=96=89=20=EB=B0=8F=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 25c52addb..f31c43bb0 100644 --- a/src/App.js +++ b/src/App.js @@ -1,9 +1,15 @@ -import { Console } from "@woowacourse/mission-utils"; +import { Console, Random } from "@woowacourse/mission-utils"; const LOTTO_PRICE = 1000; +const LOTTO_MIN = 1; +const LOTTO_MAX = 45; class App { async run() { const purchaseAmount = await this.#readPurchaseAmount(); + const ticketCount = purchaseAmount / LOTTO_PRICE; + + const tickets = this.#publishLottos(ticketCount); + this.#printTickets(tickets); } async #readPurchaseAmount() { @@ -31,6 +37,24 @@ class App { } return amount; } + + #publishLottos(count) { + const tickets = []; + for (let i = 0; i < count; i += 1) { + const numbers = Random.pickUniqueNumbersInRange(LOTTO_MIN, LOTTO_MAX, 6); + const sorted = [...numbers].sort((a, b) => a - b); + new Lotto(sorted); + tickets.push(sorted); + } + return tickets; + } + + #printTickets(tickets) { + Console.print(`${tickets.length}๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.`); + for (const ticket of tickets) { + Console.print(`[${ticket.join(", ")}]`); + } + } } export default App; From d438f6868cabddf1a7df7f77c41211dd84bf3320 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Sun, 2 Nov 2025 21:27:27 +0900 Subject: [PATCH 06/20] =?UTF-8?q?chore(types):=20Random=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=95=EC=9D=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- types/woowacourse__mission-utils.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/types/woowacourse__mission-utils.d.ts b/types/woowacourse__mission-utils.d.ts index 005746455..fa45d5336 100644 --- a/types/woowacourse__mission-utils.d.ts +++ b/types/woowacourse__mission-utils.d.ts @@ -3,4 +3,13 @@ declare module "@woowacourse/mission-utils" { readLineAsync(prompt?: string): Promise; print(message: string): void; }; + + export const Random: { + pickNumberInRange(minInclusive: number, maxInclusive: number): number; + pickUniqueNumbersInRange( + minInclusive: number, + maxInclusive: number, + count: number + ): number[]; + }; } From 0881663e083a7ad1462abca63cfea458f70bd028 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 02:03:17 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat(app):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=EC=99=80=20=EB=B3=B4=EB=84=88=EC=8A=A4=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=A5,=20=EC=9E=AC=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=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/App.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/App.js b/src/App.js index f31c43bb0..4cf6243bc 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,9 @@ class App { const tickets = this.#publishLottos(ticketCount); this.#printTickets(tickets); + + const winningNumbers = await this.#readWinningNumbers(); + const bonusNumber = await this.#readBonusNumber(winningNumbers); } async #readPurchaseAmount() { @@ -55,6 +58,66 @@ class App { Console.print(`[${ticket.join(", ")}]`); } } + + async #readWinningNumbers() { + Console.print(""); + while (true) { + const input = await Console.readLineAsync("๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); + try { + return this.#parseWinningNumbers(input); + } catch (error) { + Console.print(error.message); + } + } + } + + #parseWinningNumbers(input) { + const raw = (input ?? "").trim(); + const parts = raw.split(",").map((v) => v.trim()); + if (parts.length !== 6) { + throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + const numbers = parts.map((v) => Number(v)); + if ( + !numbers.every( + (n) => Number.isInteger(n) && n >= LOTTO_MIN && n <= LOTTO_MAX + ) + ) { + throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + const unique = new Set(numbers); + if (unique.size !== 6) { + throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return numbers; + } + + async #readBonusNumber(winningNumbers) { + Console.print(""); + while (true) { + const input = await Console.readLineAsync("๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); + try { + return this.#parseBonusNumber(input, winningNumbers); + } catch (error) { + Console.print(error.message); + } + } + } + + #parseBonusNumber(input, winningNumbers) { + const raw = (input ?? "").trim(); + if (!/^\d+$/.test(raw)) { + throw new Error("[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + const bonus = Number(raw); + if (bonus < LOTTO_MIN || bonus > LOTTO_MAX) { + throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (winningNumbers.includes(bonus)) { + throw new Error("[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return bonus; + } } export default App; From 081f18a64bb37c808431df2cfc81444bcb826ecd Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 16:19:48 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat(app):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EB=B0=8F=20=EC=88=98=EC=9D=B5=EB=A5=A0=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/App.js b/src/App.js index 4cf6243bc..046ee69d2 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,21 @@ import { Console, Random } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; const LOTTO_PRICE = 1000; const LOTTO_MIN = 1; const LOTTO_MAX = 45; + +const PRIZE_TABLE = { + three: { match: 3, money: 5000, label: "3๊ฐœ ์ผ์น˜ (5,000์›) - " }, + four: { match: 4, money: 50000, label: "4๊ฐœ ์ผ์น˜ (50,000์›) - " }, + five: { match: 5, money: 1500000, label: "5๊ฐœ ์ผ์น˜ (1,500,000์›) - " }, + fiveBonus: { + match: 5, + money: 30000000, + label: "5๊ฐœ ์ผ์น˜, ๋ณด๋„ˆ์Šค ๋ณผ ์ผ์น˜ (30,000,000์›) - ", + }, + six: { match: 6, money: 2000000000, label: "6๊ฐœ ์ผ์น˜ (2,000,000,000์›) - " }, +}; class App { async run() { const purchaseAmount = await this.#readPurchaseAmount(); @@ -13,6 +26,9 @@ class App { const winningNumbers = await this.#readWinningNumbers(); const bonusNumber = await this.#readBonusNumber(winningNumbers); + + const stats = this.#calculateStats(tickets, winningNumbers, bonusNumber); + this.#printStats(stats, purchaseAmount); } async #readPurchaseAmount() { @@ -118,6 +134,83 @@ class App { } return bonus; } + + #calculateStats(tickets, winningNumbers, bonusNumber) { + const stats = { + three: 0, + four: 0, + five: 0, + fiveBonus: 0, + six: 0, + }; + + for (const ticket of tickets) { + const matchCount = this.#countMatch(ticket, winningNumbers); + const hasBonus = ticket.includes(bonusNumber); + this.#applyStat(stats, matchCount, hasBonus); + } + return stats; + } + + #countMatch(ticket, winningNumbers) { + let count = 0; + for (const num of ticket) { + if (winningNumbers.includes(num)) { + count += 1; + } + } + return count; + } + + #applyStat(stats, matchCount, hasBonus) { + if (matchCount === 6) { + stats.six += 1; + return; + } + if (matchCount === 5 && hasBonus) { + stats.fiveBonus += 1; + return; + } + if (matchCount === 5) { + stats.five += 1; + return; + } + + if (matchCount === 4) { + stats.four += 1; + return; + } + if (matchCount === 3) { + stats.three += 1; + } + } + + #printStats(stats, purchaseAmount) { + Console.print(""); + Console.print("๋‹น์ฒจ ํ†ต๊ณ„"); + Console.print("---"); + Console.print(`${PRIZE_TABLE.three.label}${stats.three}๊ฐœ`); + Console.print(`${PRIZE_TABLE.four.label}${stats.four}๊ฐœ`); + Console.print(`${PRIZE_TABLE.five.label}${stats.five}๊ฐœ`); + Console.print(`${PRIZE_TABLE.fiveBonus.label}${stats.fiveBonus}๊ฐœ`); + Console.print(`${PRIZE_TABLE.six.label}${stats.six}๊ฐœ`); + + const profitRate = this.#calculateProfitRate(stats, purchaseAmount); + Console.print(`์ด ์ˆ˜์ต๋ฅ ์€ ${profitRate}%์ž…๋‹ˆ๋‹ค.`); + } + + #calculateProfitRate(stats, purchaseAmount) { + const totalPrize = + stats.three * PRIZE_TABLE.three.money + + stats.four * PRIZE_TABLE.four.money + + stats.five * PRIZE_TABLE.five.money + + stats.fiveBonus * PRIZE_TABLE.fiveBonus.money + + stats.six * PRIZE_TABLE.six.money; + + const rate = (totalPrize / purchaseAmount) * 100; + const rounded = Math.round(rate * 10) / 10; + return rounded; + } } export default App; From 379eceb71e849b415c744c9b09bf3972b9269c2c Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 17:15:31 +0900 Subject: [PATCH 09/20] =?UTF-8?q?test(lotto):=20=EA=B8=88=EC=95=A1,=20?= =?UTF-8?q?=EB=8B=B9=EC=B2=A8=EB=B2=88=ED=98=B8,=20=EB=B3=B4=EB=84=88?= =?UTF-8?q?=EC=8A=A4=EB=B2=88=ED=98=B8=20=EC=9E=AC=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/ApplicationTest.js | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 872380c9c..990a4b060 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -94,4 +94,56 @@ describe("๋กœ๋˜ ํ…Œ์ŠคํŠธ", () => { test("์˜ˆ์™ธ ํ…Œ์ŠคํŠธ", async () => { await runException("1000j"); }); + + test("๊ตฌ์ž… ๊ธˆ์•ก์ด 1000์› ๋‹จ์œ„๊ฐ€ ์•„๋‹ˆ๋ฉด ๋‹ค์‹œ ์ž…๋ ฅ์„ ๋ฐ›๋Š”๋‹ค", async () => { + const logSpy = getLogSpy(); + mockRandoms([ + [1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18], + [19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30], + ]); + mockQuestions(["8500", "2000", "1,2,3,4,5,6", "7"]); + + const app = new App(); + await app.run(); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ๋‹จ์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + ); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("2๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.") + ); + }); + + test("๋‹น์ฒจ ๋ฒˆํ˜ธ๊ฐ€ 6๊ฐœ๊ฐ€ ์•„๋‹ˆ๋ฉด ๋‹ค์‹œ ์ž…๋ ฅ์„ ๋ฐ›๋Š”๋‹ค", async () => { + const logSpy = getLogSpy(); + mockRandoms([[1, 2, 3, 4, 5, 6]]); + mockQuestions(["1000", "1,2,3,4,5", "1,2,3,4,5,6", "7"]); + + const app = new App(); + await app.run(); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + ); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("๋‹น์ฒจ ํ†ต๊ณ„")); + }); + + test("๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๊ฐ€ ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋˜๋ฉด ๋‹ค์‹œ ์ž…๋ ฅ์„ ๋ฐ›๋Š”๋‹ค", async () => { + const logSpy = getLogSpy(); + mockRandoms([[1, 2, 3, 4, 5, 6]]); + mockQuestions(["1000", "1,2,3,4,5,6", "3", "7"]); + + const app = new App(); + await app.run(); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining( + "[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + ) + ); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("๋‹น์ฒจ ํ†ต๊ณ„")); + }); }); From 75477a8b8de35fbde2d2fad060ff6a4df2e66c72 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 17:31:01 +0900 Subject: [PATCH 10/20] =?UTF-8?q?fix(app):=20=EC=88=98=EC=9D=B5=EB=A5=A0?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=ED=98=95=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 046ee69d2..68f3a078b 100644 --- a/src/App.js +++ b/src/App.js @@ -209,7 +209,10 @@ class App { const rate = (totalPrize / purchaseAmount) * 100; const rounded = Math.round(rate * 10) / 10; - return rounded; + return new Intl.NumberFormat("ko-KR", { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }).format(rounded); } } From 4676e41d5dbce0ac3ef080708afa350ce9666f51 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 18:00:23 +0900 Subject: [PATCH 11/20] =?UTF-8?q?fix(app):=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=9B=84=20=ED=95=9C=20=EC=A4=84=20=EA=B3=B5=EB=B0=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=A1=9C=20=EC=B6=9C=EB=A0=A5=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/App.js b/src/App.js index 68f3a078b..b62edbd1a 100644 --- a/src/App.js +++ b/src/App.js @@ -20,6 +20,7 @@ class App { async run() { const purchaseAmount = await this.#readPurchaseAmount(); const ticketCount = purchaseAmount / LOTTO_PRICE; + Console.print(""); const tickets = this.#publishLottos(ticketCount); this.#printTickets(tickets); @@ -33,7 +34,7 @@ class App { async #readPurchaseAmount() { while (true) { - const input = await Console.readLineAsync("๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); + const input = await Console.readLineAsync("๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n"); try { return this.#parsePurchaseAmount(input); } catch (error) { @@ -78,7 +79,7 @@ class App { async #readWinningNumbers() { Console.print(""); while (true) { - const input = await Console.readLineAsync("๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); + const input = await Console.readLineAsync("๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n"); try { return this.#parseWinningNumbers(input); } catch (error) { @@ -111,7 +112,9 @@ class App { async #readBonusNumber(winningNumbers) { Console.print(""); while (true) { - const input = await Console.readLineAsync("๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); + const input = await Console.readLineAsync( + "๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n" + ); try { return this.#parseBonusNumber(input, winningNumbers); } catch (error) { From 85d88a63d3cbf28cc6051d5f2f52d93bae83261f Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 20:12:14 +0900 Subject: [PATCH 12/20] =?UTF-8?q?refactor(app):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=ED=8C=8C=EC=8B=B1=20=EB=B0=8F=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/App.js b/src/App.js index b62edbd1a..c88eebd06 100644 --- a/src/App.js +++ b/src/App.js @@ -94,7 +94,25 @@ class App { if (parts.length !== 6) { throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - const numbers = parts.map((v) => Number(v)); + + const numbers = parts.map((v) => this.#parseNumber(v)); + this.#validateLottoNumbers(numbers); + + const unique = new Set(numbers); + if (unique.size !== 6) { + throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return numbers; + } + + #parseNumber(input) { + if (!/^\d+$/.test(input)) { + throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + return Number(input); + } + + #validateLottoNumbers(numbers) { if ( !numbers.every( (n) => Number.isInteger(n) && n >= LOTTO_MIN && n <= LOTTO_MAX @@ -102,11 +120,6 @@ class App { ) { throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - const unique = new Set(numbers); - if (unique.size !== 6) { - throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - return numbers; } async #readBonusNumber(winningNumbers) { @@ -125,13 +138,9 @@ class App { #parseBonusNumber(input, winningNumbers) { const raw = (input ?? "").trim(); - if (!/^\d+$/.test(raw)) { - throw new Error("[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - const bonus = Number(raw); - if (bonus < LOTTO_MIN || bonus > LOTTO_MAX) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } + const bonus = this.#parseNumber(raw); + this.#validateLottoNumbers([bonus]); + if (winningNumbers.includes(bonus)) { throw new Error("[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } From a0258043f40f1b46597b1eef88ea0c1c4f6910f8 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 20:31:31 +0900 Subject: [PATCH 13/20] =?UTF-8?q?test(lotto):=20Lotto=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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__/LottoTest.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..d96a7f8b6 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -14,5 +14,16 @@ describe("๋กœ๋˜ ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ", () => { }).toThrow("[ERROR]"); }); - // TODO: ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ + test("๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ 6๊ฐœ ๋ณด๋‹ค ์ ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.", () => { + expect(() => new Lotto([1, 2, 3, 4, 5]).toThrow("[ERROR]")); + }); + + test("๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ 1~45 ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.", () => { + expect(() => new Lotto([0, 2, 3, 4, 5, 6])).toThrow("[ERROR]"); + expect(() => new Lotto([1, 2, 3, 4, 5, 100])).toThrow("[ERROR]"); + }); + + test("์œ ํšจํ•œ ๋กœ๋˜ ๋ฒˆํ˜ธ 6๊ฐœ๋ผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.", () => { + expect(() => new Lotto([1, 2, 3, 4, 5, 6])).not.toThrow(); + }); }); From 739027ae85d617bdddd49e218865e2e4ea3e4d46 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 20:38:11 +0900 Subject: [PATCH 14/20] =?UTF-8?q?refactor(app):=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=EB=90=9C=20=EB=B3=80=EC=88=98=EB=AA=85=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EA=B0=80=EB=8F=85=EC=84=B1=20?= =?UTF-8?q?=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 45 ++++++++++++++++++++++++--------------------- src/Lotto.js | 4 +++- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/App.js b/src/App.js index c88eebd06..bab4d1fe7 100644 --- a/src/App.js +++ b/src/App.js @@ -44,11 +44,11 @@ class App { } #parsePurchaseAmount(input) { - const raw = (input ?? "").trim(); - if (!/^\d+$/.test(raw)) { + const trimmedInput = (input ?? "").trim(); + if (!/^\d+$/.test(trimmedInput)) { throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - const amount = Number(raw); + const amount = Number(trimmedInput); if (amount < LOTTO_PRICE) { throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } @@ -62,7 +62,7 @@ class App { const tickets = []; for (let i = 0; i < count; i += 1) { const numbers = Random.pickUniqueNumbersInRange(LOTTO_MIN, LOTTO_MAX, 6); - const sorted = [...numbers].sort((a, b) => a - b); + const sorted = [...numbers].sort((first, second) => first - second); new Lotto(sorted); tickets.push(sorted); } @@ -89,13 +89,15 @@ class App { } #parseWinningNumbers(input) { - const raw = (input ?? "").trim(); - const parts = raw.split(",").map((v) => v.trim()); + const trimmedInput = (input ?? "").trim(); + const parts = trimmedInput.split(",").map((value) => value.trim()); if (parts.length !== 6) { throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - const numbers = parts.map((v) => this.#parseNumber(v)); + const numbers = parts.map((numberString) => + this.#parseNumber(numberString) + ); this.#validateLottoNumbers(numbers); const unique = new Set(numbers); @@ -115,7 +117,8 @@ class App { #validateLottoNumbers(numbers) { if ( !numbers.every( - (n) => Number.isInteger(n) && n >= LOTTO_MIN && n <= LOTTO_MAX + (number) => + Number.isInteger(number) && number >= LOTTO_MIN && number <= LOTTO_MAX ) ) { throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); @@ -137,8 +140,8 @@ class App { } #parseBonusNumber(input, winningNumbers) { - const raw = (input ?? "").trim(); - const bonus = this.#parseNumber(raw); + const trimmedInput = (input ?? "").trim(); + const bonus = this.#parseNumber(trimmedInput); this.#validateLottoNumbers([bonus]); if (winningNumbers.includes(bonus)) { @@ -165,34 +168,34 @@ class App { } #countMatch(ticket, winningNumbers) { - let count = 0; - for (const num of ticket) { - if (winningNumbers.includes(num)) { - count += 1; + let matchedCount = 0; + for (const number of ticket) { + if (winningNumbers.includes(number)) { + matchedCount += 1; } } - return count; + return matchedCount; } - #applyStat(stats, matchCount, hasBonus) { - if (matchCount === 6) { + #applyStat(stats, matchedCount, hasBonus) { + if (matchedCount === 6) { stats.six += 1; return; } - if (matchCount === 5 && hasBonus) { + if (matchedCount === 5 && hasBonus) { stats.fiveBonus += 1; return; } - if (matchCount === 5) { + if (matchedCount === 5) { stats.five += 1; return; } - if (matchCount === 4) { + if (matchedCount === 4) { stats.four += 1; return; } - if (matchCount === 3) { + if (matchedCount === 3) { stats.three += 1; } } diff --git a/src/Lotto.js b/src/Lotto.js index 5ba39b5c4..e1782b339 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -12,7 +12,9 @@ class Lotto { } if ( - !numbers.every((num) => Number.isInteger(num) && num >= 1 && num <= 45) + !numbers.every( + (number) => Number.isInteger(number) && number >= 1 && number <= 45 + ) ) { throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } From e68d447b3bb144857f4f14120c62a7e8f210cb74 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 21:02:21 +0900 Subject: [PATCH 15/20] =?UTF-8?q?refactor(constants):=20=EB=A1=9C=EB=98=90?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=83=81=EC=88=98=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8B=B9=EC=B2=A8=20=EA=B8=88=EC=95=A1=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/constants.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/constants/constants.js diff --git a/src/constants/constants.js b/src/constants/constants.js new file mode 100644 index 000000000..8ca9e7c04 --- /dev/null +++ b/src/constants/constants.js @@ -0,0 +1,16 @@ +export const LOTTO_PRICE = 1000; +export const LOTTO_MIN = 1; +export const LOTTO_MAX = 45; +export const LOTTO_COUNT = 6; + +export const PRIZE_TABLE = { + three: { match: 3, money: 5000, label: "3๊ฐœ ์ผ์น˜ (5,000์›) - " }, + four: { match: 4, money: 50000, label: "4๊ฐœ ์ผ์น˜ (50,000์›) - " }, + five: { match: 5, money: 1500000, label: "5๊ฐœ ์ผ์น˜ (1,500,000์›) - " }, + fiveBonus: { + match: 5, + money: 30000000, + label: "5๊ฐœ ์ผ์น˜, ๋ณด๋„ˆ์Šค ๋ณผ ์ผ์น˜ (30,000,000์›) - ", + }, + six: { match: 6, money: 2000000000, label: "6๊ฐœ ์ผ์น˜ (2,000,000,000์›) - " }, +}; From 7890fa8645fc722df1a4689e291d4d61d6f7a8e8 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 21:03:09 +0900 Subject: [PATCH 16/20] =?UTF-8?q?refactor(constants):=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=EB=A5=BC=20=EC=83=81=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 49 ++++++++++++++++++--------------------- src/Lotto.js | 16 ++++++++----- src/constants/messages.js | 13 +++++++++++ 3 files changed, 46 insertions(+), 32 deletions(-) create mode 100644 src/constants/messages.js diff --git a/src/App.js b/src/App.js index bab4d1fe7..4b050c56a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,21 +1,14 @@ import { Console, Random } from "@woowacourse/mission-utils"; +import { + LOTTO_COUNT, + LOTTO_MAX, + LOTTO_MIN, + LOTTO_PRICE, + PRIZE_TABLE, +} from "./constants/constants.js"; +import { ERROR_MESSAGES } from "./constants/messages.js"; import Lotto from "./Lotto.js"; -const LOTTO_PRICE = 1000; -const LOTTO_MIN = 1; -const LOTTO_MAX = 45; - -const PRIZE_TABLE = { - three: { match: 3, money: 5000, label: "3๊ฐœ ์ผ์น˜ (5,000์›) - " }, - four: { match: 4, money: 50000, label: "4๊ฐœ ์ผ์น˜ (50,000์›) - " }, - five: { match: 5, money: 1500000, label: "5๊ฐœ ์ผ์น˜ (1,500,000์›) - " }, - fiveBonus: { - match: 5, - money: 30000000, - label: "5๊ฐœ ์ผ์น˜, ๋ณด๋„ˆ์Šค ๋ณผ ์ผ์น˜ (30,000,000์›) - ", - }, - six: { match: 6, money: 2000000000, label: "6๊ฐœ ์ผ์น˜ (2,000,000,000์›) - " }, -}; class App { async run() { const purchaseAmount = await this.#readPurchaseAmount(); @@ -46,14 +39,14 @@ class App { #parsePurchaseAmount(input) { const trimmedInput = (input ?? "").trim(); if (!/^\d+$/.test(trimmedInput)) { - throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_NUMBER); } const amount = Number(trimmedInput); if (amount < LOTTO_PRICE) { - throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_TOO_LOW); } if (amount % LOTTO_PRICE !== 0) { - throw new Error("[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ๋‹จ์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_DIVISIBLE); } return amount; } @@ -61,7 +54,11 @@ class App { #publishLottos(count) { const tickets = []; for (let i = 0; i < count; i += 1) { - const numbers = Random.pickUniqueNumbersInRange(LOTTO_MIN, LOTTO_MAX, 6); + const numbers = Random.pickUniqueNumbersInRange( + LOTTO_MIN, + LOTTO_MAX, + LOTTO_COUNT + ); const sorted = [...numbers].sort((first, second) => first - second); new Lotto(sorted); tickets.push(sorted); @@ -91,8 +88,8 @@ class App { #parseWinningNumbers(input) { const trimmedInput = (input ?? "").trim(); const parts = trimmedInput.split(",").map((value) => value.trim()); - if (parts.length !== 6) { - throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + if (parts.length !== LOTTO_COUNT) { + throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_COUNT_INVALID); } const numbers = parts.map((numberString) => @@ -101,15 +98,15 @@ class App { this.#validateLottoNumbers(numbers); const unique = new Set(numbers); - if (unique.size !== 6) { - throw new Error("[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + if (unique.size !== LOTTO_COUNT) { + throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_DUPLICATE); } return numbers; } #parseNumber(input) { if (!/^\d+$/.test(input)) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE); } return Number(input); } @@ -121,7 +118,7 @@ class App { Number.isInteger(number) && number >= LOTTO_MIN && number <= LOTTO_MAX ) ) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE); } } @@ -145,7 +142,7 @@ class App { this.#validateLottoNumbers([bonus]); if (winningNumbers.includes(bonus)) { - throw new Error("[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + throw new Error(ERROR_MESSAGES.BONUS_NUMBER_DUPLICATE); } return bonus; } diff --git a/src/Lotto.js b/src/Lotto.js index e1782b339..58f11c5bc 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,3 +1,6 @@ +import { LOTTO_COUNT, LOTTO_MAX, LOTTO_MIN } from "./constants/constants.js"; +import { ERROR_MESSAGES } from "./constants/messages.js"; + class Lotto { #numbers; @@ -7,21 +10,22 @@ class Lotto { } #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + if (numbers.length !== LOTTO_COUNT) { + throw new Error(ERROR_MESSAGES.LOTTO_NUMBERS_COUNT_INVALID); } if ( !numbers.every( - (number) => Number.isInteger(number) && number >= 1 && number <= 45 + (number) => + Number.isInteger(number) && number >= LOTTO_MIN && number <= LOTTO_MAX ) ) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE); } const uniqueCount = new Set(numbers).size; - if (uniqueCount !== 6) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + if (uniqueCount !== LOTTO_COUNT) { + throw new Error(ERROR_MESSAGES.LOTTO_NUMBERS_DUPLICATE); } } } diff --git a/src/constants/messages.js b/src/constants/messages.js new file mode 100644 index 000000000..3f8add169 --- /dev/null +++ b/src/constants/messages.js @@ -0,0 +1,13 @@ +export const ERROR_MESSAGES = { + PURCHASE_AMOUNT_NOT_NUMBER: "[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + PURCHASE_AMOUNT_TOO_LOW: "[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + PURCHASE_AMOUNT_NOT_DIVISIBLE: "[ERROR] ๊ตฌ์ž… ๊ธˆ์•ก์€ 1,000์› ๋‹จ์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + WINNING_NUMBERS_COUNT_INVALID: "[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + WINNING_NUMBERS_DUPLICATE: "[ERROR] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + LOTTO_NUMBER_OUT_OF_RANGE: + "[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1๋ถ€ํ„ฐ 45 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + LOTTO_NUMBERS_COUNT_INVALID: "[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + LOTTO_NUMBERS_DUPLICATE: "[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + BONUS_NUMBER_DUPLICATE: + "[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", +}; From 8b3e5603823128fdcf031d910df5812b632a06b0 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 21:31:03 +0900 Subject: [PATCH 17/20] =?UTF-8?q?refactor(constants):=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81=EC=88=98=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/messages.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/constants/messages.js b/src/constants/messages.js index 3f8add169..188affaa0 100644 --- a/src/constants/messages.js +++ b/src/constants/messages.js @@ -11,3 +11,9 @@ export const ERROR_MESSAGES = { BONUS_NUMBER_DUPLICATE: "[ERROR] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", }; + +export const INPUT_MESSAGES = { + PURCHASE_AMOUNT: "๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n", + WINNING_NUMBERS: "๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n", + BONUS_NUMBER: "๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n", +}; From 1aa4cd7dd20102bfc05813b5da5abb668532a72f Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 21:41:11 +0900 Subject: [PATCH 18/20] =?UTF-8?q?refactor(utils):=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?printNewLine=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/output.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/utils/output.js diff --git a/src/utils/output.js b/src/utils/output.js new file mode 100644 index 000000000..7eb191361 --- /dev/null +++ b/src/utils/output.js @@ -0,0 +1,5 @@ +import { Console } from "@woowacourse/mission-utils"; + +export const printNewLine = () => { + Console.print(""); +}; From 7ee378353ea75e952031cb20b2f642697d193b31 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 21:54:44 +0900 Subject: [PATCH 19/20] =?UTF-8?q?refactor(app):=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EC=99=80=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=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 --- src/App.js | 242 +++++--------------------------------------- src/InputHandler.js | 117 +++++++++++++++++++++ src/LottoService.js | 115 +++++++++++++++++++++ 3 files changed, 256 insertions(+), 218 deletions(-) create mode 100644 src/InputHandler.js create mode 100644 src/LottoService.js diff --git a/src/App.js b/src/App.js index 4b050c56a..1e13c9daa 100644 --- a/src/App.js +++ b/src/App.js @@ -1,230 +1,36 @@ -import { Console, Random } from "@woowacourse/mission-utils"; -import { - LOTTO_COUNT, - LOTTO_MAX, - LOTTO_MIN, - LOTTO_PRICE, - PRIZE_TABLE, -} from "./constants/constants.js"; -import { ERROR_MESSAGES } from "./constants/messages.js"; -import Lotto from "./Lotto.js"; +import { LOTTO_PRICE } from "./constants/constants.js"; +import InputHandler from "./InputHandler.js"; +import LottoService from "./LottoService.js"; +import { printNewLine } from "./utils/output.js"; class App { - async run() { - const purchaseAmount = await this.#readPurchaseAmount(); - const ticketCount = purchaseAmount / LOTTO_PRICE; - Console.print(""); - - const tickets = this.#publishLottos(ticketCount); - this.#printTickets(tickets); - - const winningNumbers = await this.#readWinningNumbers(); - const bonusNumber = await this.#readBonusNumber(winningNumbers); - - const stats = this.#calculateStats(tickets, winningNumbers, bonusNumber); - this.#printStats(stats, purchaseAmount); - } - - async #readPurchaseAmount() { - while (true) { - const input = await Console.readLineAsync("๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n"); - try { - return this.#parsePurchaseAmount(input); - } catch (error) { - Console.print(error.message); - } - } - } - - #parsePurchaseAmount(input) { - const trimmedInput = (input ?? "").trim(); - if (!/^\d+$/.test(trimmedInput)) { - throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_NUMBER); - } - const amount = Number(trimmedInput); - if (amount < LOTTO_PRICE) { - throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_TOO_LOW); - } - if (amount % LOTTO_PRICE !== 0) { - throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_DIVISIBLE); - } - return amount; - } + #inputHandler; + #lottoService; - #publishLottos(count) { - const tickets = []; - for (let i = 0; i < count; i += 1) { - const numbers = Random.pickUniqueNumbersInRange( - LOTTO_MIN, - LOTTO_MAX, - LOTTO_COUNT - ); - const sorted = [...numbers].sort((first, second) => first - second); - new Lotto(sorted); - tickets.push(sorted); - } - return tickets; + constructor() { + this.#inputHandler = new InputHandler(); + this.#lottoService = new LottoService(); } - #printTickets(tickets) { - Console.print(`${tickets.length}๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.`); - for (const ticket of tickets) { - Console.print(`[${ticket.join(", ")}]`); - } - } - - async #readWinningNumbers() { - Console.print(""); - while (true) { - const input = await Console.readLineAsync("๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n"); - try { - return this.#parseWinningNumbers(input); - } catch (error) { - Console.print(error.message); - } - } - } + async run() { + const purchaseAmount = await this.#inputHandler.readPurchaseAmount(); + const ticketCount = purchaseAmount / LOTTO_PRICE; + printNewLine(); - #parseWinningNumbers(input) { - const trimmedInput = (input ?? "").trim(); - const parts = trimmedInput.split(",").map((value) => value.trim()); - if (parts.length !== LOTTO_COUNT) { - throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_COUNT_INVALID); - } + const tickets = this.#lottoService.publishLottos(ticketCount); + this.#lottoService.printTickets(tickets); - const numbers = parts.map((numberString) => - this.#parseNumber(numberString) + const winningNumbers = await this.#inputHandler.readWinningNumbers(); + const bonusNumber = await this.#inputHandler.readBonusNumber( + winningNumbers ); - this.#validateLottoNumbers(numbers); - const unique = new Set(numbers); - if (unique.size !== LOTTO_COUNT) { - throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_DUPLICATE); - } - return numbers; - } - - #parseNumber(input) { - if (!/^\d+$/.test(input)) { - throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE); - } - return Number(input); - } - - #validateLottoNumbers(numbers) { - if ( - !numbers.every( - (number) => - Number.isInteger(number) && number >= LOTTO_MIN && number <= LOTTO_MAX - ) - ) { - throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE); - } - } - - async #readBonusNumber(winningNumbers) { - Console.print(""); - while (true) { - const input = await Console.readLineAsync( - "๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n" - ); - try { - return this.#parseBonusNumber(input, winningNumbers); - } catch (error) { - Console.print(error.message); - } - } - } - - #parseBonusNumber(input, winningNumbers) { - const trimmedInput = (input ?? "").trim(); - const bonus = this.#parseNumber(trimmedInput); - this.#validateLottoNumbers([bonus]); - - if (winningNumbers.includes(bonus)) { - throw new Error(ERROR_MESSAGES.BONUS_NUMBER_DUPLICATE); - } - return bonus; - } - - #calculateStats(tickets, winningNumbers, bonusNumber) { - const stats = { - three: 0, - four: 0, - five: 0, - fiveBonus: 0, - six: 0, - }; - - for (const ticket of tickets) { - const matchCount = this.#countMatch(ticket, winningNumbers); - const hasBonus = ticket.includes(bonusNumber); - this.#applyStat(stats, matchCount, hasBonus); - } - return stats; - } - - #countMatch(ticket, winningNumbers) { - let matchedCount = 0; - for (const number of ticket) { - if (winningNumbers.includes(number)) { - matchedCount += 1; - } - } - return matchedCount; - } - - #applyStat(stats, matchedCount, hasBonus) { - if (matchedCount === 6) { - stats.six += 1; - return; - } - if (matchedCount === 5 && hasBonus) { - stats.fiveBonus += 1; - return; - } - if (matchedCount === 5) { - stats.five += 1; - return; - } - - if (matchedCount === 4) { - stats.four += 1; - return; - } - if (matchedCount === 3) { - stats.three += 1; - } - } - - #printStats(stats, purchaseAmount) { - Console.print(""); - Console.print("๋‹น์ฒจ ํ†ต๊ณ„"); - Console.print("---"); - Console.print(`${PRIZE_TABLE.three.label}${stats.three}๊ฐœ`); - Console.print(`${PRIZE_TABLE.four.label}${stats.four}๊ฐœ`); - Console.print(`${PRIZE_TABLE.five.label}${stats.five}๊ฐœ`); - Console.print(`${PRIZE_TABLE.fiveBonus.label}${stats.fiveBonus}๊ฐœ`); - Console.print(`${PRIZE_TABLE.six.label}${stats.six}๊ฐœ`); - - const profitRate = this.#calculateProfitRate(stats, purchaseAmount); - Console.print(`์ด ์ˆ˜์ต๋ฅ ์€ ${profitRate}%์ž…๋‹ˆ๋‹ค.`); - } - - #calculateProfitRate(stats, purchaseAmount) { - const totalPrize = - stats.three * PRIZE_TABLE.three.money + - stats.four * PRIZE_TABLE.four.money + - stats.five * PRIZE_TABLE.five.money + - stats.fiveBonus * PRIZE_TABLE.fiveBonus.money + - stats.six * PRIZE_TABLE.six.money; - - const rate = (totalPrize / purchaseAmount) * 100; - const rounded = Math.round(rate * 10) / 10; - return new Intl.NumberFormat("ko-KR", { - minimumFractionDigits: 1, - maximumFractionDigits: 1, - }).format(rounded); + const stats = this.#lottoService.calculateStats( + tickets, + winningNumbers, + bonusNumber + ); + this.#lottoService.printStats(stats, purchaseAmount); } } diff --git a/src/InputHandler.js b/src/InputHandler.js new file mode 100644 index 000000000..bfc192676 --- /dev/null +++ b/src/InputHandler.js @@ -0,0 +1,117 @@ +import { Console } from "@woowacourse/mission-utils"; +import { + LOTTO_COUNT, + LOTTO_MAX, + LOTTO_MIN, + LOTTO_PRICE, + NUMBER_REGEX, +} from "./constants/constants.js"; +import { ERROR_MESSAGES, INPUT_MESSAGES } from "./constants/messages.js"; +import { printNewLine } from "./utils/output.js"; + +class InputHandler { + async readPurchaseAmount() { + while (true) { + const input = await Console.readLineAsync(INPUT_MESSAGES.PURCHASE_AMOUNT); + try { + return this.#parsePurchaseAmount(input); + } catch (error) { + Console.print(error.message); + } + } + } + + #trimInput(input) { + return (input ?? "").trim(); + } + + #parseNumber(input, errorMessage = ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE) { + if (!NUMBER_REGEX.test(input)) { + throw new Error(errorMessage); + } + return Number(input); + } + + #parsePurchaseAmount(input) { + const trimmedInput = this.#trimInput(input); + const amount = this.#parseNumber( + trimmedInput, + ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_NUMBER + ); + + if (amount < LOTTO_PRICE) { + throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_TOO_LOW); + } + if (amount % LOTTO_PRICE !== 0) { + throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_DIVISIBLE); + } + return amount; + } + + async readWinningNumbers() { + printNewLine(); + while (true) { + const input = await Console.readLineAsync(INPUT_MESSAGES.WINNING_NUMBERS); + try { + return this.#parseWinningNumbers(input); + } catch (error) { + Console.print(error.message); + } + } + } + + #parseWinningNumbers(input) { + const trimmedInput = this.#trimInput(input); + const parts = trimmedInput.split(",").map((value) => value.trim()); + if (parts.length !== LOTTO_COUNT) { + throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_COUNT_INVALID); + } + + const numbers = parts.map((numberString) => + this.#parseNumber(numberString) + ); + this.#validateLottoNumbers(numbers); + + const unique = new Set(numbers); + if (unique.size !== LOTTO_COUNT) { + throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_DUPLICATE); + } + return numbers; + } + + #validateLottoNumbers(numbers) { + if ( + !numbers.every( + (number) => + Number.isInteger(number) && number >= LOTTO_MIN && number <= LOTTO_MAX + ) + ) { + throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE); + } + } + + async readBonusNumber(winningNumbers) { + printNewLine(); + while (true) { + const input = await Console.readLineAsync(INPUT_MESSAGES.BONUS_NUMBER); + try { + return this.#parseBonusNumber(input, winningNumbers); + } catch (error) { + Console.print(error.message); + } + } + } + + #parseBonusNumber(input, winningNumbers) { + const trimmedInput = this.#trimInput(input); + const bonus = this.#parseNumber(trimmedInput); + this.#validateLottoNumbers([bonus]); + + if (winningNumbers.includes(bonus)) { + throw new Error(ERROR_MESSAGES.BONUS_NUMBER_DUPLICATE); + } + return bonus; + } +} + +export default InputHandler; diff --git a/src/LottoService.js b/src/LottoService.js new file mode 100644 index 000000000..3399ecfac --- /dev/null +++ b/src/LottoService.js @@ -0,0 +1,115 @@ +import { Console, Random } from "@woowacourse/mission-utils"; +import { + LOTTO_COUNT, + LOTTO_MAX, + LOTTO_MIN, + PRIZE_TABLE, +} from "./constants/constants.js"; +import Lotto from "./Lotto.js"; +import { printNewLine } from "./utils/output.js"; + +class LottoService { + publishLottos(count) { + const tickets = []; + for (let i = 0; i < count; i += 1) { + const numbers = Random.pickUniqueNumbersInRange( + LOTTO_MIN, + LOTTO_MAX, + LOTTO_COUNT + ); + const sorted = [...numbers].sort((first, second) => first - second); + new Lotto(sorted); + tickets.push(sorted); + } + return tickets; + } + + calculateStats(tickets, winningNumbers, bonusNumber) { + const stats = { + three: 0, + four: 0, + five: 0, + fiveBonus: 0, + six: 0, + }; + + for (const ticket of tickets) { + const matchCount = this.#countMatch(ticket, winningNumbers); + const hasBonus = ticket.includes(bonusNumber); + this.#applyStat(stats, matchCount, hasBonus); + } + return stats; + } + + #countMatch(ticket, winningNumbers) { + let matchedCount = 0; + for (const number of ticket) { + if (winningNumbers.includes(number)) { + matchedCount += 1; + } + } + return matchedCount; + } + + #applyStat(stats, matchCount, hasBonus) { + if (matchCount === 6) { + stats.six += 1; + return; + } + if (matchCount === 5 && hasBonus) { + stats.fiveBonus += 1; + return; + } + if (matchCount === 5) { + stats.five += 1; + return; + } + + if (matchCount === 4) { + stats.four += 1; + return; + } + if (matchCount === 3) { + stats.three += 1; + } + } + + printTickets(tickets) { + Console.print(`${tickets.length}๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.`); + for (const ticket of tickets) { + Console.print(`[${ticket.join(", ")}]`); + } + } + + printStats(stats, purchaseAmount) { + printNewLine(); + Console.print("๋‹น์ฒจ ํ†ต๊ณ„"); + Console.print("---"); + Console.print(`${PRIZE_TABLE.three.label}${stats.three}๊ฐœ`); + Console.print(`${PRIZE_TABLE.four.label}${stats.four}๊ฐœ`); + Console.print(`${PRIZE_TABLE.five.label}${stats.five}๊ฐœ`); + Console.print(`${PRIZE_TABLE.fiveBonus.label}${stats.fiveBonus}๊ฐœ`); + Console.print(`${PRIZE_TABLE.six.label}${stats.six}๊ฐœ`); + + const profitRate = this.#calculateProfitRate(stats, purchaseAmount); + Console.print(`์ด ์ˆ˜์ต๋ฅ ์€ ${profitRate}%์ž…๋‹ˆ๋‹ค.`); + } + + #calculateProfitRate(stats, purchaseAmount) { + const totalPrize = + stats.three * PRIZE_TABLE.three.money + + stats.four * PRIZE_TABLE.four.money + + stats.five * PRIZE_TABLE.five.money + + stats.fiveBonus * PRIZE_TABLE.fiveBonus.money + + stats.six * PRIZE_TABLE.six.money; + + const rate = (totalPrize / purchaseAmount) * 100; + const rounded = Math.round(rate * 10) / 10; + return new Intl.NumberFormat("ko-KR", { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }).format(rounded); + } +} + +export default LottoService; From f26d1ab07ef987cb5df6fbbd960f6c086bb64d26 Mon Sep 17 00:00:00 2001 From: Youna Joo Date: Mon, 3 Nov 2025 21:57:21 +0900 Subject: [PATCH 20/20] =?UTF-8?q?docs(readme):=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88=EC=B2=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- src/constants/constants.js | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b373d9b79..c1c1b3d37 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ### 1๏ธโƒฃ ๋กœ๋˜ ๊ตฌ์ž… ๊ธˆ์•ก ์ž…๋ ฅ -- [x] `Console.readLineAsync("๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")`๋กœ ๊ธˆ์•ก ์ž…๋ ฅ +- [x] ๊ธˆ์•ก ์ž…๋ ฅ ๋ฐ›๊ธฐ - [x] 1,000์› ๋‹จ์œ„์ธ์ง€ ๊ฒ€์ฆ - [x] 1,000์› ๋ฏธ๋งŒ ๋˜๋Š” 1,000์œผ๋กœ ๋‚˜๋ˆ„์–ด๋–จ์–ด์ง€์ง€ ์•Š์œผ๋ฉด `[ERROR]` ์ถœ๋ ฅ ํ›„ **ํ•ด๋‹น ์ž…๋ ฅ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ›๊ธฐ** @@ -17,7 +17,7 @@ ### 2๏ธโƒฃ ๋กœ๋˜ ๋ฐœํ–‰ - [x] ๊ตฌ์ž… ๊ธˆ์•ก รท 1,000 = ๋ฐœํ–‰ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ -- [x] `MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6)` ์œผ๋กœ ๋กœ๋˜ ๋ฒˆํ˜ธ ์ƒ์„ฑ +- [x] ๋žœ๋ค์œผ๋กœ ๋กœ๋˜ ๋ฒˆํ˜ธ ์ƒ์„ฑ - [x] 1๊ฐœ ๋กœ๋˜๋Š” ์ค‘๋ณต ์—†๋Š” 6๊ฐœ ๋ฒˆํ˜ธ - [x] ๋ฒˆํ˜ธ๋Š” **์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ**ํ•ด์„œ ๋ณด๊ด€ - [x] `N๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.` ์ถœ๋ ฅ ํ›„ ๊ฐ ๋กœ๋˜ ๋ฒˆํ˜ธ๋ฅผ `[1, 2, 3, 4, 5, 6]` ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅ @@ -26,7 +26,7 @@ ### 3๏ธโƒฃ ๋‹น์ฒจ ๋ฒˆํ˜ธ ์ž…๋ ฅ -- [x] `Console.readLineAsync("๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")` +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ›๊ธฐ - [x] ์‰ผํ‘œ(,) ๊ธฐ์ค€์œผ๋กœ 6๊ฐœ์ธ์ง€ ๊ฒ€์ฆ - [x] ๊ฐ ์ˆซ์ž๊ฐ€ 1~45 ๋ฒ”์œ„์ธ์ง€ ๊ฒ€์ฆ - [x] ์ค‘๋ณต ์ˆซ์ž ์—†๋„๋ก ๊ฒ€์ฆ @@ -36,7 +36,7 @@ ### 4๏ธโƒฃ ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ž…๋ ฅ -- [x] `Console.readLineAsync("๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")` +- [x] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ›๊ธฐ - [x] 1~45 ๋ฒ”์œ„ ๊ฒ€์ฆ - [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ **๊ฒน์น˜์ง€ ์•Š๋„๋ก** ๊ฒ€์ฆ - [x] ์ž˜๋ชป๋˜๋ฉด `[ERROR] ...` ์ถœ๋ ฅ ํ›„ **๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ›๊ธฐ** diff --git a/src/constants/constants.js b/src/constants/constants.js index 8ca9e7c04..e366c16c4 100644 --- a/src/constants/constants.js +++ b/src/constants/constants.js @@ -3,6 +3,8 @@ export const LOTTO_MIN = 1; export const LOTTO_MAX = 45; export const LOTTO_COUNT = 6; +export const NUMBER_REGEX = /^\d+$/; + export const PRIZE_TABLE = { three: { match: 3, money: 5000, label: "3๊ฐœ ์ผ์น˜ (5,000์›) - " }, four: { match: 4, money: 50000, label: "4๊ฐœ ์ผ์น˜ (50,000์›) - " },