From 2d57066c47252abff82c3eeb8e8f521cfc1061f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Wed, 29 Oct 2025 16:27:00 +0900 Subject: [PATCH 01/54] =?UTF-8?q?docs(README.md):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 15bb106b5..b867e7d27 100644 --- a/README.md +++ b/README.md @@ -1 +1,29 @@ # javascript-lotto-precourse + +## 구현할 기능 목록 + +- 로또 구입 금액 입력받기 + +- 로또 구입 갯수 출력하기 + +- 로또 구입 내역 출력하기 + - 발행한 로또 번호는 오름차순으로 정렬 + - 줄바꿈을 통해 로또 1개씩 출력 + +- 당첨 번호 입력 받기 + - 쉼표(,)를 기준으로 1~45 범위의 6개의 중복되지 않은 숫자를 입력받기 + +- 보너스 번호 입력 받기 + +- 당첨 통계 출력하기 + - 당첨내역 출력하기 + - 수익률 출력하기 (소수점 둘째자리에서 반올림) + +- 예외 + - 로또 구입 금액을 입력받을 때 입력받은 금액이 1000원으로 나누어떨어지지 않는 경우 -> 에러 + - 로또 구입 금액을 입력받을 때 입력받은 금액이 0이거나 숫자가 아닌 경우 -> 에러 + - 당첨 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 + - 당첨 번호를 입력받을 때 중복된 숫자가 입력되는 경우 -> 에러 처리 + - 당첨 번호를 입력받을 때 6개의 숫자를 입력받지 않은 경우 -> 에러 처리 + - 보너스 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 + - 보너스 번호를 입력받을 때 당첨 번호와 중복되는 경우 -> 에러 처리 From 00ca7bfe8fe6d8e2bc3728cd38c1f7f89c73a3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Sun, 2 Nov 2025 15:42:45 +0900 Subject: [PATCH 02/54] =?UTF-8?q?feat(App):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EA=B5=AC=EB=A7=A4=ED=95=A0=20=EA=B8=88=EC=95=A1=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EB=B0=9B=EA=B8=B0=20=EA=B5=AC=ED=98=84?= 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 091aa0a5d..fb8a45d7b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import { Console } from '@woowacourse/mission-utils'; + class App { - async run() {} + async run() { + const purchaseInput = + await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); + } } export default App; From 17da55d89a747934987bbd842310b10f708e3931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Sun, 2 Nov 2025 15:43:40 +0900 Subject: [PATCH 03/54] =?UTF-8?q?feat(App):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EA=B5=AC=EC=9E=85=20=EA=B0=9C=EC=88=98=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=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/App.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/App.js b/src/App.js index fb8a45d7b..499d839c7 100644 --- a/src/App.js +++ b/src/App.js @@ -5,6 +5,11 @@ class App { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); } + + getLottoCount(purchaseAmount) { + const count = Number(purchaseAmount) / 1000; + return count; + } } export default App; From de84078d7fba88cc842d04a3daf516461cbe7bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Sun, 2 Nov 2025 16:13:47 +0900 Subject: [PATCH 04/54] =?UTF-8?q?feat(App):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EA=B5=AC=EC=9E=85=20=EB=82=B4=EC=97=AD=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구매금액에 맞는 로또를 발행하는 메서드 구현 - 로또를 오름차순으로 정렬하는 메서드 구현 --- src/App.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 499d839c7..ca593b701 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import { Console } from '@woowacourse/mission-utils'; +import { Console, Random } from '@woowacourse/mission-utils'; class App { async run() { @@ -10,6 +10,22 @@ class App { const count = Number(purchaseAmount) / 1000; return count; } + + generateLottos(lottoCount) { + const result = []; + + for (let i = 0; i < lottoCount; i++) { + result.push( + this.sortLottoNumbers(Random.pickUniqueNumbersInRange(1, 45, 6)) + ); + } + + return result; + } + + sortLottoNumbers(lotto) { + return lotto.sort((a, b) => a - b); + } } export default App; From 9778058e88f841930d51f696fa09d852758a45f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Sun, 2 Nov 2025 17:03:40 +0900 Subject: [PATCH 05/54] =?UTF-8?q?feat(App):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=8B=B9=EC=B2=A8=EB=B2=88=ED=98=B8=20=EB=B0=8F=20=EB=B3=B4?= =?UTF-8?q?=EB=84=88=EC=8A=A4=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EB=B0=9B=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/App.js b/src/App.js index ca593b701..d3e1db83a 100644 --- a/src/App.js +++ b/src/App.js @@ -4,6 +4,12 @@ class App { async run() { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); + + const winnerLottoNumbersInput = + await Console.readLineAsync('당첨 번호를 입력해 주세요.\n'); + + const bonusLottoNumberInput = + await Console.readLineAsync('보너스 번호를 입력해 주세요.\n'); } getLottoCount(purchaseAmount) { From b899d5afd1fec6d261578e9864c05354eea04dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 11:56:52 +0900 Subject: [PATCH 06/54] =?UTF-8?q?docs(README.md):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b867e7d27..1b36dae88 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ - 보너스 번호 입력 받기 - 당첨 통계 출력하기 + - 당첨 번호를 구분자 쉼표(,)를 기준으로 구분 + - 로또, 당첨 번호, 보너스 번호를 가지고 해당 로또가 맞힌 갯수와 보너스 번호를 맞힌 여부를 반환하는 메서드 (개별 로또 결과 계산) + - 모든 로또에 대해 위의 결과를 계산, 맞힌 갯수가 3,4,5,5+보너스,6일 경우 각각 카운트 저장 + - 수익률 계산 - 당첨내역 출력하기 - 수익률 출력하기 (소수점 둘째자리에서 반올림) @@ -23,6 +27,7 @@ - 로또 구입 금액을 입력받을 때 입력받은 금액이 1000원으로 나누어떨어지지 않는 경우 -> 에러 - 로또 구입 금액을 입력받을 때 입력받은 금액이 0이거나 숫자가 아닌 경우 -> 에러 - 당첨 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 + - 당첨 번호를 입력받을 때, 숫자로 변환되지 않는 경우(NaN) -> 에러 처리 - 당첨 번호를 입력받을 때 중복된 숫자가 입력되는 경우 -> 에러 처리 - 당첨 번호를 입력받을 때 6개의 숫자를 입력받지 않은 경우 -> 에러 처리 - 보너스 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 From df434b21e6d4322a53e4d033f3bd005cb3be8623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 12:01:56 +0900 Subject: [PATCH 07/54] =?UTF-8?q?feat(App):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(?= =?UTF-8?q?=EC=89=BC=ED=91=9C=20=EA=B5=AC=EB=B6=84=20=EB=B0=8F=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=EB=B3=80=ED=99=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/App.js b/src/App.js index d3e1db83a..ec46c09af 100644 --- a/src/App.js +++ b/src/App.js @@ -32,6 +32,10 @@ class App { sortLottoNumbers(lotto) { return lotto.sort((a, b) => a - b); } + + parseWinningNumbers(winningNumbers) { + return winningNumbers.split(',').map((s) => Number(s)); + } } export default App; From 8e3a45df87d31ce327f18803833b337917cbb0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 12:16:57 +0900 Subject: [PATCH 08/54] =?UTF-8?q?feat(App):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=EC=99=80=20=EB=8B=B9=EC=B2=A8/=EB=B3=B4?= =?UTF-8?q?=EB=84=88=EC=8A=A4=20=EB=B2=88=ED=98=B8=EB=A5=BC=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=ED=95=B4=20=EC=9D=BC=EC=B9=98=20=EA=B0=9C=EC=88=98=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B3=B4=EB=84=88=EC=8A=A4=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/App.js b/src/App.js index ec46c09af..1f66fe80c 100644 --- a/src/App.js +++ b/src/App.js @@ -36,6 +36,18 @@ class App { parseWinningNumbers(winningNumbers) { return winningNumbers.split(',').map((s) => Number(s)); } + + compareWithWinningNumbers(lotto, winningNumbers, bonusNumber) { + let matchCount = 0; + let hasBonus = false; + + for (let i = 0; i < lotto.length; i++) { + if (winningNumbers.includes(lotto[i])) matchCount++; + if (lotto[i] === bonusNumber) hasBonus = true; + } + + return { matchCount, hasBonus }; + } } export default App; From 1c43834cdaedc1ea179ef782f814e8d240538023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 12:23:10 +0900 Subject: [PATCH 09/54] =?UTF-8?q?feat(App):=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EB=A1=9C=EB=98=90=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=ED=95=B4=EC=84=9C=20match3~match6=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=B9=B4=EC=9A=B4=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=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/App.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/App.js b/src/App.js index 1f66fe80c..1e1cc4827 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,14 @@ import { Console, Random } from '@woowacourse/mission-utils'; class App { + winningRank = { + match3: 0, + match4: 0, + match5: 0, + match5AndBonus: 0, + match6: 0, + }; + async run() { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); @@ -14,6 +22,7 @@ class App { getLottoCount(purchaseAmount) { const count = Number(purchaseAmount) / 1000; + this.winningRank.match3++; return count; } @@ -48,6 +57,22 @@ class App { return { matchCount, hasBonus }; } + + updateWinningStatistics(lottos, winningNumbers, bonusNumber) { + for (const lotto of lottos) { + const { matchCount, hasBonus } = this.compareWithWinningNumbers( + lotto, + winningNumbers, + bonusNumber + ); + + if (matchCount === 3) this.winningRank.match3++; + if (matchCount === 4) this.winningRank.match4++; + if (matchCount === 5 && hasBonus) this.winningRank.match5++; + if (matchCount === 5 && !hasBonus) this.winningRank.match5AndBonus++; + if (matchCount === 6) this.winningRank.match6++; + } + } } export default App; From 64b9796faaf59482af4c19a7e9e79f1fc9180bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 12:42:57 +0900 Subject: [PATCH 10/54] =?UTF-8?q?feat(App):=20=EC=B4=9D=20=EB=8B=B9?= =?UTF-8?q?=EC=B2=A8=EA=B8=88=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=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/App.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/App.js b/src/App.js index 1e1cc4827..3f61127f4 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,14 @@ class App { match6: 0, }; + prizeTable = { + 1: 2000000000, + 2: 30000000, + 3: 1500000, + 4: 50000, + 5: 5000, + }; + async run() { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); @@ -73,6 +81,21 @@ class App { if (matchCount === 6) this.winningRank.match6++; } } + + getTotalPrize() { + const prizeMap = { + match3: this.prizeTable[5], + match4: this.prizeTable[4], + match5: this.prizeTable[3], + match5AndBonus: this.prizeTable[2], + match6: this.prizeTable[1], + }; + + return Object.entries(this.winningRank).reduce( + (sum, [rank, count]) => sum + (prizeMap[rank] ?? 0) * count, + 0 + ); + } } export default App; From 7202db25f9d418dcbc1615889426238d645b4fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 12:43:37 +0900 Subject: [PATCH 11/54] =?UTF-8?q?feat(App):=20=EC=88=98=EC=9D=B5=EB=A5=A0?= =?UTF-8?q?=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/App.js b/src/App.js index 3f61127f4..e191288a7 100644 --- a/src/App.js +++ b/src/App.js @@ -96,6 +96,12 @@ class App { 0 ); } + + calculateProfitRate(purchaseAmount) { + const totalPrize = this.getTotalPrize(); + const profitRate = (totalPrize / purchaseAmount) * 100; + return profitRate.toFixed(2); + } } export default App; From f4f3e8366051bb202f662f0b94cf4048d05a46a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 12:44:47 +0900 Subject: [PATCH 12/54] =?UTF-8?q?feat(App):=20=EB=8B=B9=EC=B2=A8=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=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/App.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/App.js b/src/App.js index e191288a7..550ef4ba4 100644 --- a/src/App.js +++ b/src/App.js @@ -102,6 +102,20 @@ class App { const profitRate = (totalPrize / purchaseAmount) * 100; return profitRate.toFixed(2); } + + printStatistics(totalPrize, profitRate) { + Console.print('\n당첨 통계'); + Console.print('---'); + Console.print(`3개 일치 (5,000원) - ${this.winningRank.match3}개`); + Console.print(`4개 일치 (50,000원) - ${this.winningRank.match4}개`); + Console.print(`5개 일치 (1,500,000원) - ${this.winningRank.match5}개`); + Console.print( + `5개 일치, 보너스 볼 일치 (30,000,000원) - ${this.winningRank.match5AndBonus}개` + ); + Console.print(`6개 일치 (2,000,000,000원) - ${this.winningRank.match6}개`); + Console.print(`총 당첨금: ${totalPrize.toLocaleString()}원`); + Console.print(`총 수익률은 ${profitRate}%입니다.`); + } } export default App; From b918ce9c61da0d4b1b5980082e9e9957741cf1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 13:40:25 +0900 Subject: [PATCH 13/54] =?UTF-8?q?docs(README.md):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 1b36dae88..3e1af6933 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ - 당첨내역 출력하기 - 수익률 출력하기 (소수점 둘째자리에서 반올림) +- 리팩토링 + - App에 있는 로또 관련 로직 로또 클래스에 옮기기 + - 예외 - 로또 구입 금액을 입력받을 때 입력받은 금액이 1000원으로 나누어떨어지지 않는 경우 -> 에러 - 로또 구입 금액을 입력받을 때 입력받은 금액이 0이거나 숫자가 아닌 경우 -> 에러 From 2bfcbb643d7ef9a64a3ec300af26aa925e713582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 14:20:57 +0900 Subject: [PATCH 14/54] =?UTF-8?q?feat(App):=20run=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EB=A1=9C=EB=98=90=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EC=A0=84=EC=B2=B4=20=ED=9D=90=EB=A6=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/App.js b/src/App.js index 550ef4ba4..58957bf38 100644 --- a/src/App.js +++ b/src/App.js @@ -21,11 +21,27 @@ class App { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); - const winnerLottoNumbersInput = - await Console.readLineAsync('당첨 번호를 입력해 주세요.\n'); + const lottoCount = this.getLottoCount(purchaseInput); + const lottos = this.generateLottos(lottoCount); + Console.print(`${lottoCount}개를 구매했습니다.\n`); + lottos.forEach((lotto) => Console.print(`[${lotto.join(', ')}]`)); - const bonusLottoNumberInput = - await Console.readLineAsync('보너스 번호를 입력해 주세요.\n'); + const winnerLottoNumbersInput = await Console.readLineAsync( + '\n당첨 번호를 입력해 주세요.\n' + ); + const bonusLottoNumberInput = await Console.readLineAsync( + '\n보너스 번호를 입력해 주세요.\n' + ); + + const winningNumbers = this.parseWinningNumbers(winnerLottoNumbersInput); + const bonusNumber = Number(bonusLottoNumberInput); + + this.updateWinningStatistics(lottos, winningNumbers, bonusNumber); + + const totalPrize = this.getTotalPrize(); + const profitRate = this.calculateProfitRate(purchaseInput); + + this.printStatistics(totalPrize, profitRate); } getLottoCount(purchaseAmount) { @@ -100,7 +116,7 @@ class App { calculateProfitRate(purchaseAmount) { const totalPrize = this.getTotalPrize(); const profitRate = (totalPrize / purchaseAmount) * 100; - return profitRate.toFixed(2); + return profitRate.toFixed(1); } printStatistics(totalPrize, profitRate) { From b0922566f0edcd9d1d06ad8c173b6241b596cf16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 14:34:44 +0900 Subject: [PATCH 15/54] =?UTF-8?q?feat(Lotto):=20Lotto=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - constructor에서 로또 번호 오름차순 정렬 - generateLottos: 여러 개 로또 생성 - getNumbers: 로또 번호 반환(private라 해당 메서드 필요) - compareWithWinningNumbers: 당첨 번호와 로또 비교 --- src/Lotto.js | 18 ------------------ src/lotto/Lotto.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 18 deletions(-) delete mode 100644 src/Lotto.js create mode 100644 src/lotto/Lotto.js diff --git a/src/Lotto.js b/src/Lotto.js deleted file mode 100644 index cb0b1527e..000000000 --- a/src/Lotto.js +++ /dev/null @@ -1,18 +0,0 @@ -class Lotto { - #numbers; - - constructor(numbers) { - this.#validate(numbers); - this.#numbers = numbers; - } - - #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); - } - } - - // TODO: 추가 기능 구현 -} - -export default Lotto; diff --git a/src/lotto/Lotto.js b/src/lotto/Lotto.js new file mode 100644 index 000000000..9d1e10280 --- /dev/null +++ b/src/lotto/Lotto.js @@ -0,0 +1,42 @@ +import { Random } from '@woowacourse/mission-utils'; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers.sort((a, b) => a - b); + } + + #validate(numbers) { + if (numbers.length !== 6) { + throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); + } + } + + static generateLottos(lottoCount) { + const result = []; + + for (let i = 0; i < lottoCount; i++) { + const numbers = Random.pickUniqueNumbersInRange(1, 45, 6); + result.push(new Lotto(numbers)); + } + + return result; + } + + getNumbers() { + return this.#numbers; + } + + compareWithWinningNumbers(winningNumbers, bonusNumber) { + const matchCount = this.#numbers.filter((n) => + winningNumbers.includes(n) + ).length; + const hasBonus = this.#numbers.includes(bonusNumber); + + return { matchCount, hasBonus }; + } +} + +export default Lotto; From 14f99d1520afff196e1c3f72bec2a7876f32b0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 15:00:27 +0900 Subject: [PATCH 16/54] =?UTF-8?q?refactor(App):=20Lotto=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App에서 로또 생성, 번호 정렬, 비교 로직 제거 - Lotto.generateLottos 및 Lotto.compareWithWinningNumbers 활용 --- src/App.js | 46 ++++++++++------------------------------------ 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/src/App.js b/src/App.js index 58957bf38..074cc096c 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,5 @@ -import { Console, Random } from '@woowacourse/mission-utils'; +import { Console } from '@woowacourse/mission-utils'; +import Lotto from './lotto/Lotto.js'; class App { winningRank = { @@ -22,9 +23,12 @@ class App { await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); const lottoCount = this.getLottoCount(purchaseInput); - const lottos = this.generateLottos(lottoCount); + const lottos = Lotto.generateLottos(lottoCount); + Console.print(`${lottoCount}개를 구매했습니다.\n`); - lottos.forEach((lotto) => Console.print(`[${lotto.join(', ')}]`)); + lottos.forEach((lotto) => + Console.print(`[${lotto.getNumbers().join(', ')}]`) + ); const winnerLottoNumbersInput = await Console.readLineAsync( '\n당첨 번호를 입력해 주세요.\n' @@ -46,54 +50,24 @@ class App { getLottoCount(purchaseAmount) { const count = Number(purchaseAmount) / 1000; - this.winningRank.match3++; return count; } - generateLottos(lottoCount) { - const result = []; - - for (let i = 0; i < lottoCount; i++) { - result.push( - this.sortLottoNumbers(Random.pickUniqueNumbersInRange(1, 45, 6)) - ); - } - - return result; - } - - sortLottoNumbers(lotto) { - return lotto.sort((a, b) => a - b); - } - parseWinningNumbers(winningNumbers) { return winningNumbers.split(',').map((s) => Number(s)); } - compareWithWinningNumbers(lotto, winningNumbers, bonusNumber) { - let matchCount = 0; - let hasBonus = false; - - for (let i = 0; i < lotto.length; i++) { - if (winningNumbers.includes(lotto[i])) matchCount++; - if (lotto[i] === bonusNumber) hasBonus = true; - } - - return { matchCount, hasBonus }; - } - updateWinningStatistics(lottos, winningNumbers, bonusNumber) { for (const lotto of lottos) { - const { matchCount, hasBonus } = this.compareWithWinningNumbers( - lotto, + const { matchCount, hasBonus } = lotto.compareWithWinningNumbers( winningNumbers, bonusNumber ); if (matchCount === 3) this.winningRank.match3++; if (matchCount === 4) this.winningRank.match4++; - if (matchCount === 5 && hasBonus) this.winningRank.match5++; - if (matchCount === 5 && !hasBonus) this.winningRank.match5AndBonus++; + if (matchCount === 5 && !hasBonus) this.winningRank.match5++; + if (matchCount === 5 && hasBonus) this.winningRank.match5AndBonus++; if (matchCount === 6) this.winningRank.match6++; } } From 0be76933f30084e1cec214a81e2118513c13ae3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 15:23:15 +0900 Subject: [PATCH 17/54] =?UTF-8?q?docs(README.md):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3e1af6933..e92108eb7 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,16 @@ - 리팩토링 - App에 있는 로또 관련 로직 로또 클래스에 옮기기 -- 예외 - - 로또 구입 금액을 입력받을 때 입력받은 금액이 1000원으로 나누어떨어지지 않는 경우 -> 에러 - - 로또 구입 금액을 입력받을 때 입력받은 금액이 0이거나 숫자가 아닌 경우 -> 에러 - - 당첨 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 - - 당첨 번호를 입력받을 때, 숫자로 변환되지 않는 경우(NaN) -> 에러 처리 - - 당첨 번호를 입력받을 때 중복된 숫자가 입력되는 경우 -> 에러 처리 - - 당첨 번호를 입력받을 때 6개의 숫자를 입력받지 않은 경우 -> 에러 처리 - - 보너스 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 - - 보너스 번호를 입력받을 때 당첨 번호와 중복되는 경우 -> 에러 처리 +- 에러 처리 + - 로또 구매 가격 입력 관련 + - 입력받은 금액이 숫자가 아닌 경우 + - 입력받은 금액이 1,000원 미만일 경우 + - 입력받은 금액이 1,000원 단위가 아닌 경우 + - 당첨 번호 관련 + - 당첨 번호가 1~45 범위를 벗어나는 경우 + - 당첨 번호가 숫자가 아닌 경우 + - 당첨 번호에 중복된 숫자가 있는 경우 + - 당첨 번호 개수가 6개가 아닌 경우 + - 보너스 번호 관련 + - 보너스 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 + - 보너스 번호를 입력받을 때 당첨 번호와 중복되는 경우 -> 에러 처리 From 233440deaf995696857d7c654512e4a707c0e4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 15:24:53 +0900 Subject: [PATCH 18/54] =?UTF-8?q?feat(App):=20=EA=B5=AC=EB=A7=A4=EA=B8=88?= =?UTF-8?q?=EC=95=A1=20=EC=9E=85=EB=A0=A5=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구매 금액이 숫자가 아닌 경우 - 구매 금액이 1,000원 미만인 경우 - 구매 금액이 1,000원 단위가 아닌 경우 --- src/App.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/App.js b/src/App.js index 074cc096c..d14d587a5 100644 --- a/src/App.js +++ b/src/App.js @@ -22,6 +22,8 @@ class App { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); + this.validatePurchaseAmount(purchaseInput); + const lottoCount = this.getLottoCount(purchaseInput); const lottos = Lotto.generateLottos(lottoCount); @@ -106,6 +108,16 @@ class App { Console.print(`총 당첨금: ${totalPrize.toLocaleString()}원`); Console.print(`총 수익률은 ${profitRate}%입니다.`); } + + validatePurchaseAmount(purchaseAmount) { + const money = Number(purchaseAmount); + if (isNaN(money)) + throw new Error('[ERROR] 구매 금액은 숫자로 입력해 주세요.'); + if (money < 1000) + throw new Error('[ERROR] 구매 금액은 1,000원 이상이어야 합니다.'); + if (money % 1000 !== 0) + throw new Error('[ERROR] 구매 금액은 1,000원 단위로 입력해 주세요.'); + } } export default App; From 3b13928039b15c3cba0990c8cc8bd6456646856d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 15:37:19 +0900 Subject: [PATCH 19/54] =?UTF-8?q?feat(App):=20=EB=8B=B9=EC=B2=A8=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=A6=9D=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 - 번호가 숫자가 아닌 경우 - 번호가 1 ~ 45 범위가 아닌 경우 - 번호가 6개가 아닌 경우 - 번호가 중복된 경우 --- src/App.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/App.js b/src/App.js index d14d587a5..e5b1a217b 100644 --- a/src/App.js +++ b/src/App.js @@ -21,12 +21,10 @@ class App { async run() { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); - this.validatePurchaseAmount(purchaseInput); const lottoCount = this.getLottoCount(purchaseInput); const lottos = Lotto.generateLottos(lottoCount); - Console.print(`${lottoCount}개를 구매했습니다.\n`); lottos.forEach((lotto) => Console.print(`[${lotto.getNumbers().join(', ')}]`) @@ -35,18 +33,18 @@ class App { const winnerLottoNumbersInput = await Console.readLineAsync( '\n당첨 번호를 입력해 주세요.\n' ); + const winningNumbers = this.parseWinningNumbers(winnerLottoNumbersInput); + this.validateWinningNumbers(winningNumbers); + const bonusLottoNumberInput = await Console.readLineAsync( '\n보너스 번호를 입력해 주세요.\n' ); - - const winningNumbers = this.parseWinningNumbers(winnerLottoNumbersInput); const bonusNumber = Number(bonusLottoNumberInput); this.updateWinningStatistics(lottos, winningNumbers, bonusNumber); const totalPrize = this.getTotalPrize(); const profitRate = this.calculateProfitRate(purchaseInput); - this.printStatistics(totalPrize, profitRate); } @@ -118,6 +116,24 @@ class App { if (money % 1000 !== 0) throw new Error('[ERROR] 구매 금액은 1,000원 단위로 입력해 주세요.'); } + + validateWinningNumbers(numbers) { + const parsedNumbers = numbers.map((n) => Number(n)); + + parsedNumbers.forEach((number) => { + if (isNaN(number)) + throw new Error('[ERROR] 당첨 번호는 숫자로 입력해 주세요.'); + if (number < 1 || number > 45) + throw new Error('[ERROR] 당첨 번호는 1 ~ 45 범위로 입력해 주세요.'); + }); + + if (parsedNumbers.length !== 6) + throw new Error('[ERROR] 당첨 번호는 6개여야 합니다.'); + + const unique = new Set(parsedNumbers); + if (unique.size !== parsedNumbers.length) + throw new Error('[ERROR] 당첨 번호는 중복될 수 없습니다.'); + } } export default App; From 777c00347744c4b4387242d01bf36f802fafd87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 15:45:37 +0900 Subject: [PATCH 20/54] =?UTF-8?q?docs(README.md):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e92108eb7..71c816575 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,11 @@ - 입력받은 금액이 1,000원 미만일 경우 - 입력받은 금액이 1,000원 단위가 아닌 경우 - 당첨 번호 관련 - - 당첨 번호가 1~45 범위를 벗어나는 경우 - 당첨 번호가 숫자가 아닌 경우 + - 당첨 번호가 1~45 범위를 벗어나는 경우 - 당첨 번호에 중복된 숫자가 있는 경우 - 당첨 번호 개수가 6개가 아닌 경우 - 보너스 번호 관련 + - 보너스 번호가 숫자가 아닌 경우 - 보너스 번호를 입력받을 때 1~45 범위를 벗어나는 경우 -> 에러 처리 - 보너스 번호를 입력받을 때 당첨 번호와 중복되는 경우 -> 에러 처리 From 80711b36bcf681f0b4f8718f0abd574fcda1fdcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 16:39:43 +0900 Subject: [PATCH 21/54] =?UTF-8?q?feat(App):=20=EB=B3=B4=EB=84=88=EC=8A=A4?= =?UTF-8?q?=20=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 번호가 숫자가 아닌 경우 - 번호가 1 ~ 45 범위가 아닌 경우 - 번호가 당첨번호와 중복되는 경우 --- src/App.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/App.js b/src/App.js index e5b1a217b..18d0ac58f 100644 --- a/src/App.js +++ b/src/App.js @@ -40,6 +40,7 @@ class App { '\n보너스 번호를 입력해 주세요.\n' ); const bonusNumber = Number(bonusLottoNumberInput); + this.validateBonusNumber(bonusNumber, winningNumbers); this.updateWinningStatistics(lottos, winningNumbers, bonusNumber); @@ -134,6 +135,18 @@ class App { if (unique.size !== parsedNumbers.length) throw new Error('[ERROR] 당첨 번호는 중복될 수 없습니다.'); } + + validateBonusNumber(number, winningNumbers) { + const bonusNumber = Number(number); + + if (isNaN(bonusNumber)) + throw new Error('[ERROR] 보너스 번호는 숫자로 입력해 주세요.'); + if (bonusNumber < 1 || bonusNumber > 45) + throw new Error('[ERROR] 보너스 번호는 1 ~ 45 범위로 입력해 주세요.'); + + if (winningNumbers.includes(bonusNumber)) + throw new Error('[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.'); + } } export default App; From 06675a672e6b98a3a2f3257785a5aaa508a2abdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:10:00 +0900 Subject: [PATCH 22/54] =?UTF-8?q?feat(LottoResult):=20LottoResult=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B8=B0=EB=B3=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lotto/LottoResult.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/lotto/LottoResult.js diff --git a/src/lotto/LottoResult.js b/src/lotto/LottoResult.js new file mode 100644 index 000000000..785c7fe4f --- /dev/null +++ b/src/lotto/LottoResult.js @@ -0,0 +1,17 @@ +class LottoResult { + #winningRank = { + match3: 0, + match4: 0, + match5: 0, + match5AndBonus: 0, + match6: 0, + }; + + #prizeTable = { + 1: 2000000000, + 2: 30000000, + 3: 1500000, + 4: 50000, + 5: 5000, + }; +} From f34ecf0d64b7123007c246dc7b1a345a807fe45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:10:45 +0900 Subject: [PATCH 23/54] =?UTF-8?q?feat(LottoResult):=20=EB=A1=9C=EB=98=90?= =?UTF-8?q?=20=ED=86=B5=EA=B3=84=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lotto/LottoResult.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/lotto/LottoResult.js b/src/lotto/LottoResult.js index 785c7fe4f..38dff8730 100644 --- a/src/lotto/LottoResult.js +++ b/src/lotto/LottoResult.js @@ -14,4 +14,19 @@ class LottoResult { 4: 50000, 5: 5000, }; + + updateWinningStatistics(lottos, winningNumbers, bonusNumber) { + for (const lotto of lottos) { + const { matchCount, hasBonus } = lotto.compareWithWinningNumbers( + winningNumbers, + bonusNumber + ); + + if (matchCount === 3) this.#winningRank.match3++; + if (matchCount === 4) this.#winningRank.match4++; + if (matchCount === 5 && !hasBonus) this.#winningRank.match5++; + if (matchCount === 5 && hasBonus) this.#winningRank.match5AndBonus++; + if (matchCount === 6) this.#winningRank.match6++; + } + } } From 274e147e9ab24b4dbc6201bf178ab207b4591ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:11:31 +0900 Subject: [PATCH 24/54] =?UTF-8?q?feat(LottoResult):=20=EC=B4=9D=20?= =?UTF-8?q?=EC=83=81=EA=B8=88=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=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/lotto/LottoResult.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/lotto/LottoResult.js b/src/lotto/LottoResult.js index 38dff8730..221f58f59 100644 --- a/src/lotto/LottoResult.js +++ b/src/lotto/LottoResult.js @@ -29,4 +29,19 @@ class LottoResult { if (matchCount === 6) this.#winningRank.match6++; } } + + getTotalPrize() { + const prizeMap = { + match3: this.#prizeTable[5], + match4: this.#prizeTable[4], + match5: this.#prizeTable[3], + match5AndBonus: this.#prizeTable[2], + match6: this.#prizeTable[1], + }; + + return Object.entries(this.winningRank).reduce( + (sum, [rank, count]) => sum + (prizeMap[rank] ?? 0) * count, + 0 + ); + } } From b88264cd7518eecd5eaee04b39c0bf705337e159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:11:52 +0900 Subject: [PATCH 25/54] =?UTF-8?q?feat(LottoResult):=20=EC=88=98=EC=9D=B5?= =?UTF-8?q?=EB=A5=A0=20=EA=B3=84=EC=82=B0=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/lotto/LottoResult.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lotto/LottoResult.js b/src/lotto/LottoResult.js index 221f58f59..5cfd15ce5 100644 --- a/src/lotto/LottoResult.js +++ b/src/lotto/LottoResult.js @@ -44,4 +44,10 @@ class LottoResult { 0 ); } + + calculateProfitRate(purchaseAmount) { + const totalPrize = this.getTotalPrize(); + const profitRate = (totalPrize / purchaseAmount) * 100; + return profitRate.toFixed(1); + } } From cd0e5ba878148c1bbe0bc2c1b1902925c25fa05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:12:47 +0900 Subject: [PATCH 26/54] =?UTF-8?q?feat(LottoResult):=20=EB=8B=B9=EC=B2=A8?= =?UTF-8?q?=20=ED=86=B5=EA=B3=84=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lotto/LottoResult.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lotto/LottoResult.js b/src/lotto/LottoResult.js index 5cfd15ce5..aa329250a 100644 --- a/src/lotto/LottoResult.js +++ b/src/lotto/LottoResult.js @@ -15,6 +15,10 @@ class LottoResult { 5: 5000, }; + getWinningRank() { + return { ...this.#winningRank }; + } + updateWinningStatistics(lottos, winningNumbers, bonusNumber) { for (const lotto of lottos) { const { matchCount, hasBonus } = lotto.compareWithWinningNumbers( From abf609cda7e3a996f6409dd30b659ad08c0fd8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:22:36 +0900 Subject: [PATCH 27/54] =?UTF-8?q?fix(LottoResult):=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lotto/LottoResult.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lotto/LottoResult.js b/src/lotto/LottoResult.js index aa329250a..39b584499 100644 --- a/src/lotto/LottoResult.js +++ b/src/lotto/LottoResult.js @@ -43,7 +43,7 @@ class LottoResult { match6: this.#prizeTable[1], }; - return Object.entries(this.winningRank).reduce( + return Object.entries(this.#winningRank).reduce( (sum, [rank, count]) => sum + (prizeMap[rank] ?? 0) * count, 0 ); @@ -55,3 +55,5 @@ class LottoResult { return profitRate.toFixed(1); } } + +export default LottoResult; From 634d81c55a846283df7eb70a9d32e7133364a968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:24:51 +0900 Subject: [PATCH 28/54] =?UTF-8?q?refactor(App):=20LottoResult=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20App=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 통계 관련 로직 분리 - App 클래스 내 불필요한 메서드 및 변수 제거 - LottoResult와 App간 연동 로직 수정 --- src/App.js | 75 ++++++++++-------------------------------------------- 1 file changed, 13 insertions(+), 62 deletions(-) diff --git a/src/App.js b/src/App.js index 18d0ac58f..2878b7b39 100644 --- a/src/App.js +++ b/src/App.js @@ -1,23 +1,8 @@ import { Console } from '@woowacourse/mission-utils'; import Lotto from './lotto/Lotto.js'; +import LottoResult from './lotto/LottoResult.js'; class App { - winningRank = { - match3: 0, - match4: 0, - match5: 0, - match5AndBonus: 0, - match6: 0, - }; - - prizeTable = { - 1: 2000000000, - 2: 30000000, - 3: 1500000, - 4: 50000, - 5: 5000, - }; - async run() { const purchaseInput = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); @@ -42,11 +27,13 @@ class App { const bonusNumber = Number(bonusLottoNumberInput); this.validateBonusNumber(bonusNumber, winningNumbers); - this.updateWinningStatistics(lottos, winningNumbers, bonusNumber); + const lottoResult = new LottoResult(); + lottoResult.updateWinningStatistics(lottos, winningNumbers, bonusNumber); + const totalPrize = lottoResult.getTotalPrize(); + const profitRate = lottoResult.calculateProfitRate(purchaseInput); + const winningRank = lottoResult.getWinningRank(); - const totalPrize = this.getTotalPrize(); - const profitRate = this.calculateProfitRate(purchaseInput); - this.printStatistics(totalPrize, profitRate); + this.printStatistics(totalPrize, profitRate, winningRank); } getLottoCount(purchaseAmount) { @@ -58,52 +45,16 @@ class App { return winningNumbers.split(',').map((s) => Number(s)); } - updateWinningStatistics(lottos, winningNumbers, bonusNumber) { - for (const lotto of lottos) { - const { matchCount, hasBonus } = lotto.compareWithWinningNumbers( - winningNumbers, - bonusNumber - ); - - if (matchCount === 3) this.winningRank.match3++; - if (matchCount === 4) this.winningRank.match4++; - if (matchCount === 5 && !hasBonus) this.winningRank.match5++; - if (matchCount === 5 && hasBonus) this.winningRank.match5AndBonus++; - if (matchCount === 6) this.winningRank.match6++; - } - } - - getTotalPrize() { - const prizeMap = { - match3: this.prizeTable[5], - match4: this.prizeTable[4], - match5: this.prizeTable[3], - match5AndBonus: this.prizeTable[2], - match6: this.prizeTable[1], - }; - - return Object.entries(this.winningRank).reduce( - (sum, [rank, count]) => sum + (prizeMap[rank] ?? 0) * count, - 0 - ); - } - - calculateProfitRate(purchaseAmount) { - const totalPrize = this.getTotalPrize(); - const profitRate = (totalPrize / purchaseAmount) * 100; - return profitRate.toFixed(1); - } - - printStatistics(totalPrize, profitRate) { + printStatistics(totalPrize, profitRate, winningRank) { Console.print('\n당첨 통계'); Console.print('---'); - Console.print(`3개 일치 (5,000원) - ${this.winningRank.match3}개`); - Console.print(`4개 일치 (50,000원) - ${this.winningRank.match4}개`); - Console.print(`5개 일치 (1,500,000원) - ${this.winningRank.match5}개`); + Console.print(`3개 일치 (5,000원) - ${winningRank.match3}개`); + Console.print(`4개 일치 (50,000원) - ${winningRank.match4}개`); + Console.print(`5개 일치 (1,500,000원) - ${winningRank.match5}개`); Console.print( - `5개 일치, 보너스 볼 일치 (30,000,000원) - ${this.winningRank.match5AndBonus}개` + `5개 일치, 보너스 볼 일치 (30,000,000원) - ${winningRank.match5AndBonus}개` ); - Console.print(`6개 일치 (2,000,000,000원) - ${this.winningRank.match6}개`); + Console.print(`6개 일치 (2,000,000,000원) - ${winningRank.match6}개`); Console.print(`총 당첨금: ${totalPrize.toLocaleString()}원`); Console.print(`총 수익률은 ${profitRate}%입니다.`); } From 8acb38f94cba4a3e94f66e32378b222e8222fa31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:34:19 +0900 Subject: [PATCH 29/54] =?UTF-8?q?docs(README.md):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 71c816575..0c691f406 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - 리팩토링 - App에 있는 로또 관련 로직 로또 클래스에 옮기기 + - App에 있는 입출력 관련 로직 view로 분리하기 - 에러 처리 - 로또 구매 가격 입력 관련 From 3b1e214b8426f4836b85ab11205b9ff6dd7a5f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:56:51 +0900 Subject: [PATCH 30/54] =?UTF-8?q?feat(InputHandler):=20App=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8D=98=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20InputHandler=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/InputHandler.js | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/view/InputHandler.js diff --git a/src/view/InputHandler.js b/src/view/InputHandler.js new file mode 100644 index 000000000..9f6ab8e8b --- /dev/null +++ b/src/view/InputHandler.js @@ -0,0 +1,65 @@ +import { Console } from '@woowacourse/mission-utils'; + +class InputHandler { + static async readPurchaseAmount() { + const input = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); + this.validatePurchaseAmount(input); + return Number(input); + } + + static async readWinningNumbers() { + const input = await Console.readLineAsync('\n당첨 번호를 입력해 주세요.\n'); + const numbers = this.parseWinningNumbers(input); + this.validateWinningNumbers(numbers); + return numbers; + } + + static async readBonusNumber(winningNumbers) { + const input = await Console.readLineAsync( + '\n보너스 번호를 입력해 주세요.\n' + ); + const bonusNumber = Number(input); + this.validateBonusNumber(bonusNumber, winningNumbers); + return bonusNumber; + } + + static parseWinningNumbers(winningNumbers) { + return winningNumbers.split(',').map((s) => Number(s)); + } + + static validatePurchaseAmount(purchaseAmount) { + const money = Number(purchaseAmount); + if (isNaN(money)) + throw new Error('[ERROR] 구매 금액은 숫자로 입력해 주세요.'); + if (money < 1000) + throw new Error('[ERROR] 구매 금액은 1,000원 이상이어야 합니다.'); + if (money % 1000 !== 0) + throw new Error('[ERROR] 구매 금액은 1,000원 단위로 입력해 주세요.'); + } + + static validateWinningNumbers(numbers) { + numbers.forEach((number) => { + if (isNaN(number)) + throw new Error('[ERROR] 당첨 번호는 숫자로 입력해 주세요.'); + if (number < 1 || number > 45) + throw new Error('[ERROR] 당첨 번호는 1 ~ 45 범위로 입력해 주세요.'); + }); + + if (numbers.length !== 6) + throw new Error('[ERROR] 당첨 번호는 6개여야 합니다.'); + const unique = new Set(numbers); + if (unique.size !== numbers.length) + throw new Error('[ERROR] 당첨 번호는 중복될 수 없습니다.'); + } + + static validateBonusNumber(bonusNumber, winningNumbers) { + if (isNaN(bonusNumber)) + throw new Error('[ERROR] 보너스 번호는 숫자로 입력해 주세요.'); + if (bonusNumber < 1 || bonusNumber > 45) + throw new Error('[ERROR] 보너스 번호는 1 ~ 45 범위로 입력해 주세요.'); + if (winningNumbers.includes(bonusNumber)) + throw new Error('[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.'); + } +} + +export default InputHandler; From 3d17645cdc1f87b62b404175daa00d46a93c57f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 19:57:50 +0900 Subject: [PATCH 31/54] =?UTF-8?q?refactor(App):=20InputHandler=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20App=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 69 ++++++++---------------------------------------------- 1 file changed, 10 insertions(+), 59 deletions(-) diff --git a/src/App.js b/src/App.js index 2878b7b39..b940a3226 100644 --- a/src/App.js +++ b/src/App.js @@ -1,12 +1,11 @@ import { Console } from '@woowacourse/mission-utils'; import Lotto from './lotto/Lotto.js'; import LottoResult from './lotto/LottoResult.js'; +import InputHandler from './view/InputHandler.js'; class App { async run() { - const purchaseInput = - await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); - this.validatePurchaseAmount(purchaseInput); + const purchaseInput = await InputHandler.readPurchaseAmount(); const lottoCount = this.getLottoCount(purchaseInput); const lottos = Lotto.generateLottos(lottoCount); @@ -15,20 +14,16 @@ class App { Console.print(`[${lotto.getNumbers().join(', ')}]`) ); - const winnerLottoNumbersInput = await Console.readLineAsync( - '\n당첨 번호를 입력해 주세요.\n' - ); - const winningNumbers = this.parseWinningNumbers(winnerLottoNumbersInput); - this.validateWinningNumbers(winningNumbers); - - const bonusLottoNumberInput = await Console.readLineAsync( - '\n보너스 번호를 입력해 주세요.\n' - ); - const bonusNumber = Number(bonusLottoNumberInput); - this.validateBonusNumber(bonusNumber, winningNumbers); + const winnerLottoNumbers = await InputHandler.readWinningNumbers(); + const bonusLottoNumber = + await InputHandler.readBonusNumber(winnerLottoNumbers); const lottoResult = new LottoResult(); - lottoResult.updateWinningStatistics(lottos, winningNumbers, bonusNumber); + lottoResult.updateWinningStatistics( + lottos, + winnerLottoNumbers, + bonusLottoNumber + ); const totalPrize = lottoResult.getTotalPrize(); const profitRate = lottoResult.calculateProfitRate(purchaseInput); const winningRank = lottoResult.getWinningRank(); @@ -41,10 +36,6 @@ class App { return count; } - parseWinningNumbers(winningNumbers) { - return winningNumbers.split(',').map((s) => Number(s)); - } - printStatistics(totalPrize, profitRate, winningRank) { Console.print('\n당첨 통계'); Console.print('---'); @@ -58,46 +49,6 @@ class App { Console.print(`총 당첨금: ${totalPrize.toLocaleString()}원`); Console.print(`총 수익률은 ${profitRate}%입니다.`); } - - validatePurchaseAmount(purchaseAmount) { - const money = Number(purchaseAmount); - if (isNaN(money)) - throw new Error('[ERROR] 구매 금액은 숫자로 입력해 주세요.'); - if (money < 1000) - throw new Error('[ERROR] 구매 금액은 1,000원 이상이어야 합니다.'); - if (money % 1000 !== 0) - throw new Error('[ERROR] 구매 금액은 1,000원 단위로 입력해 주세요.'); - } - - validateWinningNumbers(numbers) { - const parsedNumbers = numbers.map((n) => Number(n)); - - parsedNumbers.forEach((number) => { - if (isNaN(number)) - throw new Error('[ERROR] 당첨 번호는 숫자로 입력해 주세요.'); - if (number < 1 || number > 45) - throw new Error('[ERROR] 당첨 번호는 1 ~ 45 범위로 입력해 주세요.'); - }); - - if (parsedNumbers.length !== 6) - throw new Error('[ERROR] 당첨 번호는 6개여야 합니다.'); - - const unique = new Set(parsedNumbers); - if (unique.size !== parsedNumbers.length) - throw new Error('[ERROR] 당첨 번호는 중복될 수 없습니다.'); - } - - validateBonusNumber(number, winningNumbers) { - const bonusNumber = Number(number); - - if (isNaN(bonusNumber)) - throw new Error('[ERROR] 보너스 번호는 숫자로 입력해 주세요.'); - if (bonusNumber < 1 || bonusNumber > 45) - throw new Error('[ERROR] 보너스 번호는 1 ~ 45 범위로 입력해 주세요.'); - - if (winningNumbers.includes(bonusNumber)) - throw new Error('[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.'); - } } export default App; From eb9f0c65ed85651b274f0a2e6b23e913ffc9735c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 20:23:50 +0900 Subject: [PATCH 32/54] =?UTF-8?q?feat(OutputHandler):=20App=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8D=98=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?OutputHandler=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/OutputHandler.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/view/OutputHandler.js diff --git a/src/view/OutputHandler.js b/src/view/OutputHandler.js new file mode 100644 index 000000000..65178e95d --- /dev/null +++ b/src/view/OutputHandler.js @@ -0,0 +1,29 @@ +import { Console } from '@woowacourse/mission-utils'; + +class OutputHandler { + static printLottoCount(lottoCount) { + Console.print(`\n${lottoCount}개를 구매했습니다.\n`); + } + + static printLotto(lottos) { + lottos.forEach((lotto) => + Console.print(`[${lotto.getNumbers().join(', ')}]`) + ); + } + + static printStatistics(totalPrize, profitRate, winningRank) { + Console.print('\n당첨 통계'); + Console.print('---'); + Console.print(`3개 일치 (5,000원) - ${winningRank.match3}개`); + Console.print(`4개 일치 (50,000원) - ${winningRank.match4}개`); + Console.print(`5개 일치 (1,500,000원) - ${winningRank.match5}개`); + Console.print( + `5개 일치, 보너스 볼 일치 (30,000,000원) - ${winningRank.match5AndBonus}개` + ); + Console.print(`6개 일치 (2,000,000,000원) - ${winningRank.match6}개`); + Console.print(`총 당첨금: ${totalPrize.toLocaleString()}원`); + Console.print(`총 수익률은 ${profitRate}%입니다.`); + } +} + +export default OutputHandler; From 80859dd1d338b4665acf827d6b0b394cab276e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 20:25:11 +0900 Subject: [PATCH 33/54] =?UTF-8?q?refactor(App):=20OutputHandler=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?App=20=ED=81=B4=20=EB=9E=98=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/App.js b/src/App.js index b940a3226..5a52b8c97 100644 --- a/src/App.js +++ b/src/App.js @@ -1,54 +1,46 @@ -import { Console } from '@woowacourse/mission-utils'; import Lotto from './lotto/Lotto.js'; import LottoResult from './lotto/LottoResult.js'; import InputHandler from './view/InputHandler.js'; +import OutputHandler from './view/OutputHandler.js'; class App { async run() { + // 1. 구매 금액 입력 const purchaseInput = await InputHandler.readPurchaseAmount(); + // 2. 로또 생성 const lottoCount = this.getLottoCount(purchaseInput); const lottos = Lotto.generateLottos(lottoCount); - Console.print(`${lottoCount}개를 구매했습니다.\n`); - lottos.forEach((lotto) => - Console.print(`[${lotto.getNumbers().join(', ')}]`) - ); + // 3. 로또 결과 출력 + OutputHandler.printLottoCount(lottoCount); + OutputHandler.printLotto(lottos); + + // 4. 당첨 번호 입력 const winnerLottoNumbers = await InputHandler.readWinningNumbers(); const bonusLottoNumber = await InputHandler.readBonusNumber(winnerLottoNumbers); + // 5. 당첨 통계 계산 const lottoResult = new LottoResult(); lottoResult.updateWinningStatistics( lottos, winnerLottoNumbers, bonusLottoNumber ); + + // 6. 통계 출력 const totalPrize = lottoResult.getTotalPrize(); const profitRate = lottoResult.calculateProfitRate(purchaseInput); const winningRank = lottoResult.getWinningRank(); - this.printStatistics(totalPrize, profitRate, winningRank); + OutputHandler.printStatistics(totalPrize, profitRate, winningRank); } getLottoCount(purchaseAmount) { const count = Number(purchaseAmount) / 1000; return count; } - - printStatistics(totalPrize, profitRate, winningRank) { - Console.print('\n당첨 통계'); - Console.print('---'); - Console.print(`3개 일치 (5,000원) - ${winningRank.match3}개`); - Console.print(`4개 일치 (50,000원) - ${winningRank.match4}개`); - Console.print(`5개 일치 (1,500,000원) - ${winningRank.match5}개`); - Console.print( - `5개 일치, 보너스 볼 일치 (30,000,000원) - ${winningRank.match5AndBonus}개` - ); - Console.print(`6개 일치 (2,000,000,000원) - ${winningRank.match6}개`); - Console.print(`총 당첨금: ${totalPrize.toLocaleString()}원`); - Console.print(`총 수익률은 ${profitRate}%입니다.`); - } } export default App; From 43a6c137ff04588a594497b05083fd821660184a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 20:25:56 +0900 Subject: [PATCH 34/54] =?UTF-8?q?feat(LottoUtils):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EA=B5=AC=EB=A7=A4=20=EA=B0=AF=EC=88=98=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20LottoUtils=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lotto/LottoUtils.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/lotto/LottoUtils.js diff --git a/src/lotto/LottoUtils.js b/src/lotto/LottoUtils.js new file mode 100644 index 000000000..7cf7cc90f --- /dev/null +++ b/src/lotto/LottoUtils.js @@ -0,0 +1,7 @@ +class LottoUtils { + static getLottoCount(purchaseAmount) { + return Number(purchaseAmount) / 1000; + } +} + +export default LottoUtils; From 115eb365e3161a567826d867407bd03c33d20970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 20:27:36 +0900 Subject: [PATCH 35/54] =?UTF-8?q?refactor(App):=20LottoUtils=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20App=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=82=B4=EB=B6=80=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/App.js b/src/App.js index 5a52b8c97..73c88a311 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,6 @@ import Lotto from './lotto/Lotto.js'; import LottoResult from './lotto/LottoResult.js'; +import LottoUtils from './lotto/LottoUtils.js'; import InputHandler from './view/InputHandler.js'; import OutputHandler from './view/OutputHandler.js'; @@ -9,7 +10,7 @@ class App { const purchaseInput = await InputHandler.readPurchaseAmount(); // 2. 로또 생성 - const lottoCount = this.getLottoCount(purchaseInput); + const lottoCount = LottoUtils.getLottoCount(purchaseInput); const lottos = Lotto.generateLottos(lottoCount); // 3. 로또 결과 출력 @@ -36,11 +37,6 @@ class App { OutputHandler.printStatistics(totalPrize, profitRate, winningRank); } - - getLottoCount(purchaseAmount) { - const count = Number(purchaseAmount) / 1000; - return count; - } } export default App; From 23218c2badac2528faced5ee4bf5e77a6c69372f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 20:40:31 +0900 Subject: [PATCH 36/54] =?UTF-8?q?docs(README.md):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0c691f406..2b355df85 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ - App에 있는 입출력 관련 로직 view로 분리하기 - 에러 처리 + - 로또 클래스 관련 + - 로또 번호 6개가 아닌 경우(이미 구현되어 있음) + - 로또 번호가 중복되는 경우 + - 로또 번호가 1~45 범위를 벗어나는 경우 - 로또 구매 가격 입력 관련 - 입력받은 금액이 숫자가 아닌 경우 - 입력받은 금액이 1,000원 미만일 경우 From d67c27d0dcfb37c20b6f6b4910591e510e1c7a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:13:14 +0900 Subject: [PATCH 37/54] =?UTF-8?q?fix(InputHandler):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EC=9E=85=EB=A0=A5=EA=B0=92=EC=9D=84=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=20=EC=9E=85=EB=A0=A5=ED=95=98=EC=98=80?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C,=20=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8?= =?UTF-8?q?=20=EC=A2=85=EB=A3=8C=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=20=EC=A7=80=EC=A0=90=EB=B6=80=ED=84=B0=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=9E=85=EB=A0=A5=EB=B0=9B=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/InputHandler.js | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/view/InputHandler.js b/src/view/InputHandler.js index 9f6ab8e8b..1c18a627c 100644 --- a/src/view/InputHandler.js +++ b/src/view/InputHandler.js @@ -1,26 +1,43 @@ import { Console } from '@woowacourse/mission-utils'; class InputHandler { + static async readInputWithValidation(message, validateFn) { + while (true) { + try { + const input = await Console.readLineAsync(message); + validateFn(input); + return input; + } catch (error) { + Console.print(error.message); + } + } + } + static async readPurchaseAmount() { - const input = await Console.readLineAsync('구매 금액을 입력해 주세요.\n'); - this.validatePurchaseAmount(input); + const input = await this.readInputWithValidation( + '구매 금액을 입력해 주세요.\n', + this.validatePurchaseAmount + ); + return Number(input); } static async readWinningNumbers() { - const input = await Console.readLineAsync('\n당첨 번호를 입력해 주세요.\n'); - const numbers = this.parseWinningNumbers(input); - this.validateWinningNumbers(numbers); - return numbers; + const input = await this.readInputWithValidation( + '\n당첨 번호를 입력해 주세요.\n', + (raw) => this.validateWinningNumbers(this.parseWinningNumbers(raw)) + ); + + return this.parseWinningNumbers(input); } static async readBonusNumber(winningNumbers) { - const input = await Console.readLineAsync( - '\n보너스 번호를 입력해 주세요.\n' + const input = await this.readInputWithValidation( + '\n보너스 번호를 입력해 주세요.\n', + (raw) => this.validateBonusNumber(Number(raw), winningNumbers) ); - const bonusNumber = Number(input); - this.validateBonusNumber(bonusNumber, winningNumbers); - return bonusNumber; + + return Number(input); } static parseWinningNumbers(winningNumbers) { From f5cce0d1261d580b5c808a4deea08e78152c48b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:14:30 +0900 Subject: [PATCH 38/54] =?UTF-8?q?fix(OutputHandler):=20=EC=A3=BC=EC=96=B4?= =?UTF-8?q?=EC=A7=84=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=ED=98=95=EC=8B=9D=EC=97=90=20=EB=A7=9E=EA=B2=8C=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/view/OutputHandler.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/view/OutputHandler.js b/src/view/OutputHandler.js index 65178e95d..e97a0d732 100644 --- a/src/view/OutputHandler.js +++ b/src/view/OutputHandler.js @@ -2,7 +2,7 @@ import { Console } from '@woowacourse/mission-utils'; class OutputHandler { static printLottoCount(lottoCount) { - Console.print(`\n${lottoCount}개를 구매했습니다.\n`); + Console.print(`\n${lottoCount}개를 구매했습니다.`); } static printLotto(lottos) { @@ -11,7 +11,7 @@ class OutputHandler { ); } - static printStatistics(totalPrize, profitRate, winningRank) { + static printStatistics(profitRate, winningRank) { Console.print('\n당첨 통계'); Console.print('---'); Console.print(`3개 일치 (5,000원) - ${winningRank.match3}개`); @@ -21,7 +21,6 @@ class OutputHandler { `5개 일치, 보너스 볼 일치 (30,000,000원) - ${winningRank.match5AndBonus}개` ); Console.print(`6개 일치 (2,000,000,000원) - ${winningRank.match6}개`); - Console.print(`총 당첨금: ${totalPrize.toLocaleString()}원`); Console.print(`총 수익률은 ${profitRate}%입니다.`); } } From 0776931c3f91c772548c50e61b530f29ecaaf1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:15:07 +0900 Subject: [PATCH 39/54] =?UTF-8?q?feat(Lotto):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A4=91=EB=B3=B5=20=EB=B0=8F=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EA=B2=80=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lotto/Lotto.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lotto/Lotto.js b/src/lotto/Lotto.js index 9d1e10280..d8138983a 100644 --- a/src/lotto/Lotto.js +++ b/src/lotto/Lotto.js @@ -12,6 +12,15 @@ class Lotto { if (numbers.length !== 6) { throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); } + + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== numbers.length) + throw new Error('[ERROR] 로또 번호는 중복될 수 없습니다.'); + + for (let i = 0; i < numbers.length; i++) { + if (numbers[i] < 1 || numbers[i] > 45) + throw new Error('[ERROR] 로또 번호는 1~45 범위여야 합니다.'); + } } static generateLottos(lottoCount) { From 022425157aa181e659953880276833c15986ae6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:15:45 +0900 Subject: [PATCH 40/54] =?UTF-8?q?fix(App):=20OutputHandler=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=BD=94=EB=93=9C=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/App.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 73c88a311..66c860693 100644 --- a/src/App.js +++ b/src/App.js @@ -35,7 +35,7 @@ class App { const profitRate = lottoResult.calculateProfitRate(purchaseInput); const winningRank = lottoResult.getWinningRank(); - OutputHandler.printStatistics(totalPrize, profitRate, winningRank); + OutputHandler.printStatistics(profitRate, winningRank); } } From d4a9b865e01d9a29d34b66566aa41427d56c1c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:16:18 +0900 Subject: [PATCH 41/54] =?UTF-8?q?fix(LottoTest):=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..5a3d64e71 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,17 +1,17 @@ -import Lotto from "../src/Lotto"; +import Lotto from '../src/lotto/Lotto'; -describe("로또 클래스 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { +describe('로또 클래스 테스트', () => { + test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); + }).toThrow('[ERROR]'); }); // TODO: 테스트가 통과하도록 프로덕션 코드 구현 - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { + test('로또 번호에 중복된 숫자가 있으면 예외가 발생한다.', () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); + }).toThrow('[ERROR]'); }); // TODO: 추가 기능 구현에 따른 테스트 코드 작성 From a68565b657ab173444142b9c643b0df1916bf7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:32:18 +0900 Subject: [PATCH 42/54] =?UTF-8?q?test(Lotto):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=201~45=20=EB=B2=94=EC=9C=84=20=EB=B2=97?= =?UTF-8?q?=EC=96=B4=EB=82=A0=20=EB=95=8C=20=EC=98=88=EC=99=B8=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 5a3d64e71..5f77f13c3 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -15,4 +15,12 @@ describe('로또 클래스 테스트', () => { }); // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + test('로또 번호가 1~45 범위를 벗어나면 예외가 발생한다.', () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow('[ERROR]'); + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow('[ERROR]'); + }); }); From f5590291ef684411afc1ab2b82e1e01f96a33e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:32:53 +0900 Subject: [PATCH 43/54] =?UTF-8?q?test(Lotto):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A0=95=EC=83=81=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=A4=EB=A6=84=EC=B0=A8=EC=88=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 5f77f13c3..22681ec00 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -23,4 +23,9 @@ describe('로또 클래스 테스트', () => { new Lotto([1, 2, 3, 4, 5, 46]); }).toThrow('[ERROR]'); }); + + test('로또 번호가 정상적으로 생성되고 오름차순 정렬된다', () => { + const lotto = new Lotto([19, 3, 8, 21, 16, 31]); + expect(lotto.getNumbers()).toEqual([3, 8, 16, 19, 21, 31]); + }); }); From 70032b87816a15158d79a4e00bca56ad0f528145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:34:14 +0900 Subject: [PATCH 44/54] =?UTF-8?q?test(LottoUtils):=20=EA=B5=AC=EB=A7=A4=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=EA=B0=9C=EC=88=98=20=EA=B3=84=EC=82=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoUtilsTest.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 __tests__/LottoUtilsTest.js diff --git a/__tests__/LottoUtilsTest.js b/__tests__/LottoUtilsTest.js new file mode 100644 index 000000000..848e82488 --- /dev/null +++ b/__tests__/LottoUtilsTest.js @@ -0,0 +1,8 @@ +import LottoUtils from '../src/lotto/LottoUtils.js'; + +describe('LottoUtils 단위 테스트', () => { + test('구매 금액에 따라 로또 개수 계산', () => { + expect(LottoUtils.getLottoCount(1000)).toBe(1); + expect(LottoUtils.getLottoCount(8000)).toBe(8); + }); +}); From f05b571784500edafe6a42c7058dccdfa53cacc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:49:14 +0900 Subject: [PATCH 45/54] =?UTF-8?q?test(LottoResult):=20LottoResult=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EB=8B=B9=EC=B2=A8=20=ED=86=B5=EA=B3=84?= =?UTF-8?q?/=EC=B4=9D=20=EC=83=81=EA=B8=88/=EC=88=98=EC=9D=B5=EB=A5=A0=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoResultTest.js | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 __tests__/LottoResultTest.js diff --git a/__tests__/LottoResultTest.js b/__tests__/LottoResultTest.js new file mode 100644 index 000000000..e5222debf --- /dev/null +++ b/__tests__/LottoResultTest.js @@ -0,0 +1,42 @@ +import Lotto from '../src/lotto/Lotto.js'; +import LottoResult from '../src/lotto/LottoResult.js'; + +describe('LottoResult 단위 테스트', () => { + let lottoResult; + beforeEach(() => { + lottoResult = new LottoResult(); + }); + + test('당첨 통계 업데이트 및 총 상금 계산', () => { + const lottos = [ + new Lotto([1, 2, 3, 4, 5, 6]), // match6 + new Lotto([1, 2, 3, 4, 5, 7]), // match5 + bonus + new Lotto([1, 2, 3, 4, 8, 9]), // match4 + new Lotto([1, 2, 3, 10, 11, 12]), // match3 + new Lotto([13, 14, 15, 16, 17, 18]), // 0개 + ]; + + const winningNumbers = [1, 2, 3, 4, 5, 6]; + const bonusNumber = 7; + + lottoResult.updateWinningStatistics(lottos, winningNumbers, bonusNumber); + + const winningRank = lottoResult.getWinningRank(); + const totalPrize = lottoResult.getTotalPrize(); + const profitRate = lottoResult.calculateProfitRate(5000); + + // 맞은 개수 검증 + expect(winningRank.match3).toBe(1); + expect(winningRank.match4).toBe(1); + expect(winningRank.match5).toBe(0); + expect(winningRank.match5AndBonus).toBe(1); + expect(winningRank.match6).toBe(1); + + // 총 상금 검증 + const expectedTotal = 5000 + 50000 + 30000000 + 2000000000; + expect(totalPrize).toBe(expectedTotal); + + // 수익률 검증 + expect(profitRate).toBe(((expectedTotal / 5000) * 100).toFixed(1)); + }); +}); From 15604ccbc755d4d7e2cee0f605f87d8d47c2e056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:53:45 +0900 Subject: [PATCH 46/54] =?UTF-8?q?refactor(App):=20App.js=EB=A5=BC=20contro?= =?UTF-8?q?ller=20=ED=8F=B4=EB=8D=94=20=EB=82=B4=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{ => controller}/App.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/{ => controller}/App.js (81%) diff --git a/src/App.js b/src/controller/App.js similarity index 81% rename from src/App.js rename to src/controller/App.js index 66c860693..7a516a85d 100644 --- a/src/App.js +++ b/src/controller/App.js @@ -1,8 +1,8 @@ -import Lotto from './lotto/Lotto.js'; -import LottoResult from './lotto/LottoResult.js'; -import LottoUtils from './lotto/LottoUtils.js'; -import InputHandler from './view/InputHandler.js'; -import OutputHandler from './view/OutputHandler.js'; +import Lotto from '../lotto/Lotto.js'; +import LottoResult from '../lotto/LottoResult.js'; +import LottoUtils from '../lotto/LottoUtils.js'; +import InputHandler from '../view/InputHandler.js'; +import OutputHandler from '../view/OutputHandler.js'; class App { async run() { From d4568c588c0f2093f9910e20e94ba67bb5133e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 21:54:44 +0900 Subject: [PATCH 47/54] =?UTF-8?q?refactor(ApplicationTest):=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/ApplicationTest.js | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 872380c9c..adae21381 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,5 +1,5 @@ -import App from "../src/App.js"; -import { MissionUtils } from "@woowacourse/mission-utils"; +import App from '../src/controller/App.js'; +import { MissionUtils } from '@woowacourse/mission-utils'; const mockQuestions = (inputs) => { MissionUtils.Console.readLineAsync = jest.fn(); @@ -19,7 +19,7 @@ const mockRandoms = (numbers) => { }; const getLogSpy = () => { - const logSpy = jest.spyOn(MissionUtils.Console, "print"); + const logSpy = jest.spyOn(MissionUtils.Console, 'print'); logSpy.mockClear(); return logSpy; }; @@ -29,7 +29,7 @@ const runException = async (input) => { const logSpy = getLogSpy(); const RANDOM_NUMBERS_TO_END = [1, 2, 3, 4, 5, 6]; - const INPUT_NUMBERS_TO_END = ["1000", "1,2,3,4,5,6", "7"]; + const INPUT_NUMBERS_TO_END = ['1000', '1,2,3,4,5,6', '7']; mockRandoms([RANDOM_NUMBERS_TO_END]); mockQuestions([input, ...INPUT_NUMBERS_TO_END]); @@ -39,15 +39,15 @@ const runException = async (input) => { await app.run(); // then - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[ERROR]')); }; -describe("로또 테스트", () => { +describe('로또 테스트', () => { beforeEach(() => { jest.restoreAllMocks(); }); - test("기능 테스트", async () => { + test('기능 테스트', async () => { // given const logSpy = getLogSpy(); @@ -61,7 +61,7 @@ describe("로또 테스트", () => { [2, 13, 22, 32, 38, 45], [1, 3, 5, 14, 22, 45], ]); - mockQuestions(["8000", "1,2,3,4,5,6", "7"]); + mockQuestions(['8000', '1,2,3,4,5,6', '7']); // when const app = new App(); @@ -69,21 +69,21 @@ describe("로또 테스트", () => { // then const logs = [ - "8개를 구매했습니다.", - "[8, 21, 23, 41, 42, 43]", - "[3, 5, 11, 16, 32, 38]", - "[7, 11, 16, 35, 36, 44]", - "[1, 8, 11, 31, 41, 42]", - "[13, 14, 16, 38, 42, 45]", - "[7, 11, 30, 40, 42, 43]", - "[2, 13, 22, 32, 38, 45]", - "[1, 3, 5, 14, 22, 45]", - "3개 일치 (5,000원) - 1개", - "4개 일치 (50,000원) - 0개", - "5개 일치 (1,500,000원) - 0개", - "5개 일치, 보너스 볼 일치 (30,000,000원) - 0개", - "6개 일치 (2,000,000,000원) - 0개", - "총 수익률은 62.5%입니다.", + '8개를 구매했습니다.', + '[8, 21, 23, 41, 42, 43]', + '[3, 5, 11, 16, 32, 38]', + '[7, 11, 16, 35, 36, 44]', + '[1, 8, 11, 31, 41, 42]', + '[13, 14, 16, 38, 42, 45]', + '[7, 11, 30, 40, 42, 43]', + '[2, 13, 22, 32, 38, 45]', + '[1, 3, 5, 14, 22, 45]', + '3개 일치 (5,000원) - 1개', + '4개 일치 (50,000원) - 0개', + '5개 일치 (1,500,000원) - 0개', + '5개 일치, 보너스 볼 일치 (30,000,000원) - 0개', + '6개 일치 (2,000,000,000원) - 0개', + '총 수익률은 62.5%입니다.', ]; logs.forEach((log) => { @@ -91,7 +91,7 @@ describe("로또 테스트", () => { }); }); - test("예외 테스트", async () => { - await runException("1000j"); + test('예외 테스트', async () => { + await runException('1000j'); }); }); From c1eb8c3fbb83b963f7401c18f354d5afa58d0cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 22:32:35 +0900 Subject: [PATCH 48/54] =?UTF-8?q?refactor(index.js):=20App.js=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 02a1d389e..9f1977d76 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import App from "./App.js"; +import App from './controller/App.js'; const app = new App(); await app.run(); From fd3af91cc1de0a6afbb2e70bede0c2de18ed4bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 22:37:38 +0900 Subject: [PATCH 49/54] =?UTF-8?q?feat(constant):=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/constants.js | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 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..273d0fbbe --- /dev/null +++ b/src/constants/constants.js @@ -0,0 +1,51 @@ +export const LOTTO = { + NUMBER_COUNT: 6, + MIN_NUMBER: 1, + MAX_NUMBER: 45, + MIN_PURCHASE_AMOUNT: 1000, + UNIT: 1000, +}; + +export const PRIZE = { + MATCH3: 5000, + MATCH4: 50000, + MATCH5: 1500000, + MATCH5_BONUS: 30000000, + MATCH6: 2000000000, +}; + +export const MESSAGES = { + PURCHASE_AMOUNT_PROMPT: '구매 금액을 입력해 주세요.\n', + WINNING_NUMBERS_PROMPT: '\n당첨 번호를 입력해 주세요.\n', + BONUS_NUMBER_PROMPT: '\n보너스 번호를 입력해 주세요.\n', +}; + +export const ERROR_MESSAGES = { + PURCHASE_NUMBER: '[ERROR] 구매 금액은 숫자로 입력해 주세요.', + MIN_PURCHASE: '[ERROR] 구매 금액은 1,000원 이상이어야 합니다.', + UNIT_PURCHASE: '[ERROR] 구매 금액은 1,000원 단위로 입력해 주세요.', + LOTTO_NUMBER_COUNT: '[ERROR] 로또 번호는 6개여야 합니다.', + LOTTO_NUMBER_DUPLICATE: '[ERROR] 로또 번호는 중복될 수 없습니다.', + LOTTO_NUMBER_RANGE: '[ERROR] 로또 번호는 1~45 범위여야 합니다.', + WINNING_NUMBER_FORMAT: '[ERROR] 당첨 번호는 숫자로 입력해 주세요.', + WINNING_NUMBER_RANGE: '[ERROR] 당첨 번호는 1 ~ 45 범위로 입력해 주세요.', + WINNING_NUMBER_COUNT: '[ERROR] 당첨 번호는 6개여야 합니다.', + WINNING_NUMBER_DUPLICATE: '[ERROR] 당첨 번호는 중복될 수 없습니다.', + BONUS_NUMBER_FORMAT: '[ERROR] 보너스 번호는 숫자로 입력해 주세요.', + BONUS_NUMBER_RANGE: '[ERROR] 보너스 번호는 1 ~ 45 범위로 입력해 주세요.', + BONUS_NUMBER_DUPLICATE: + '[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.', +}; + +export const OUTPUT_MESSAGES = { + PURCHASED_COUNT: (count) => `\n${count}개를 구매했습니다.`, + STATISTICS_HEADER: '\n당첨 통계', + STATISTICS_SEPARATOR: '---', + STATISTICS_MATCH3: (count) => `3개 일치 (5,000원) - ${count}개`, + STATISTICS_MATCH4: (count) => `4개 일치 (50,000원) - ${count}개`, + STATISTICS_MATCH5: (count) => `5개 일치 (1,500,000원) - ${count}개`, + STATISTICS_MATCH5_BONUS: (count) => + `5개 일치, 보너스 볼 일치 (30,000,000원) - ${count}개`, + STATISTICS_MATCH6: (count) => `6개 일치 (2,000,000,000원) - ${count}개`, + PROFIT_RATE: (rate) => `총 수익률은 ${rate}%입니다.`, +}; From a922fa82597fb53338fa6b11a77f6ca4ae998caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 22:37:58 +0900 Subject: [PATCH 50/54] =?UTF-8?q?refactor(Lotto):=20=EC=83=81=EC=88=98=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/lotto/Lotto.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lotto/Lotto.js b/src/lotto/Lotto.js index d8138983a..551d0d5da 100644 --- a/src/lotto/Lotto.js +++ b/src/lotto/Lotto.js @@ -1,4 +1,5 @@ import { Random } from '@woowacourse/mission-utils'; +import { LOTTO, ERROR_MESSAGES } from '../constants/constants.js'; class Lotto { #numbers; @@ -10,16 +11,16 @@ class Lotto { #validate(numbers) { if (numbers.length !== 6) { - throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); + throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_COUNT); } const uniqueNumbers = new Set(numbers); if (uniqueNumbers.size !== numbers.length) - throw new Error('[ERROR] 로또 번호는 중복될 수 없습니다.'); + throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_DUPLICATE); for (let i = 0; i < numbers.length; i++) { - if (numbers[i] < 1 || numbers[i] > 45) - throw new Error('[ERROR] 로또 번호는 1~45 범위여야 합니다.'); + if (numbers[i] < LOTTO.MIN_NUMBER || numbers[i] > LOTTO.MAX_NUMBER) + throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_RANGE); } } @@ -27,7 +28,11 @@ class Lotto { const result = []; for (let i = 0; i < lottoCount; i++) { - const numbers = Random.pickUniqueNumbersInRange(1, 45, 6); + const numbers = Random.pickUniqueNumbersInRange( + LOTTO.MIN_NUMBER, + LOTTO.MAX_NUMBER, + LOTTO.NUMBER_COUNT + ); result.push(new Lotto(numbers)); } From 486d259004f474f59d9db82792a4cb01c1a4ff07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 22:38:10 +0900 Subject: [PATCH 51/54] =?UTF-8?q?refactor(LottoResult):=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lotto/LottoResult.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lotto/LottoResult.js b/src/lotto/LottoResult.js index 39b584499..5c912e877 100644 --- a/src/lotto/LottoResult.js +++ b/src/lotto/LottoResult.js @@ -1,3 +1,5 @@ +import { PRIZE } from '../constants/constants.js'; + class LottoResult { #winningRank = { match3: 0, @@ -8,11 +10,11 @@ class LottoResult { }; #prizeTable = { - 1: 2000000000, - 2: 30000000, - 3: 1500000, - 4: 50000, - 5: 5000, + 1: PRIZE.MATCH6, + 2: PRIZE.MATCH5_BONUS, + 3: PRIZE.MATCH5, + 4: PRIZE.MATCH4, + 5: PRIZE.MATCH3, }; getWinningRank() { From 26c15035144dbdfa69da29d720ef885216a392bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 22:38:23 +0900 Subject: [PATCH 52/54] =?UTF-8?q?refactor(InputHandler):=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/InputHandler.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/view/InputHandler.js b/src/view/InputHandler.js index 1c18a627c..8c14a9d09 100644 --- a/src/view/InputHandler.js +++ b/src/view/InputHandler.js @@ -1,4 +1,5 @@ import { Console } from '@woowacourse/mission-utils'; +import { MESSAGES, ERROR_MESSAGES } from '../constants/constants.js'; class InputHandler { static async readInputWithValidation(message, validateFn) { @@ -15,7 +16,7 @@ class InputHandler { static async readPurchaseAmount() { const input = await this.readInputWithValidation( - '구매 금액을 입력해 주세요.\n', + MESSAGES.PURCHASE_AMOUNT_PROMPT, this.validatePurchaseAmount ); @@ -24,7 +25,7 @@ class InputHandler { static async readWinningNumbers() { const input = await this.readInputWithValidation( - '\n당첨 번호를 입력해 주세요.\n', + MESSAGES.WINNING_NUMBERS_PROMPT, (raw) => this.validateWinningNumbers(this.parseWinningNumbers(raw)) ); @@ -33,7 +34,7 @@ class InputHandler { static async readBonusNumber(winningNumbers) { const input = await this.readInputWithValidation( - '\n보너스 번호를 입력해 주세요.\n', + MESSAGES.BONUS_NUMBER_PROMPT, (raw) => this.validateBonusNumber(Number(raw), winningNumbers) ); @@ -46,36 +47,31 @@ class InputHandler { static validatePurchaseAmount(purchaseAmount) { const money = Number(purchaseAmount); - if (isNaN(money)) - throw new Error('[ERROR] 구매 금액은 숫자로 입력해 주세요.'); - if (money < 1000) - throw new Error('[ERROR] 구매 금액은 1,000원 이상이어야 합니다.'); - if (money % 1000 !== 0) - throw new Error('[ERROR] 구매 금액은 1,000원 단위로 입력해 주세요.'); + if (isNaN(money)) throw new Error(ERROR_MESSAGES.PURCHASE_NUMBER); + if (money < 1000) throw new Error(ERROR_MESSAGES.MIN_PURCHASE); + if (money % 1000 !== 0) throw new Error(ERROR_MESSAGES.UNIT_PURCHASE); } static validateWinningNumbers(numbers) { numbers.forEach((number) => { - if (isNaN(number)) - throw new Error('[ERROR] 당첨 번호는 숫자로 입력해 주세요.'); + if (isNaN(number)) throw new Error(ERROR_MESSAGES.WINNING_NUMBER_FORMAT); if (number < 1 || number > 45) - throw new Error('[ERROR] 당첨 번호는 1 ~ 45 범위로 입력해 주세요.'); + throw new Error(ERROR_MESSAGES.WINNING_NUMBER_RANGE); }); if (numbers.length !== 6) - throw new Error('[ERROR] 당첨 번호는 6개여야 합니다.'); + throw new Error(ERROR_MESSAGES.WINNING_NUMBER_COUNT); const unique = new Set(numbers); if (unique.size !== numbers.length) - throw new Error('[ERROR] 당첨 번호는 중복될 수 없습니다.'); + throw new Error(ERROR_MESSAGES.WINNING_NUMBER_DUPLICATE); } static validateBonusNumber(bonusNumber, winningNumbers) { - if (isNaN(bonusNumber)) - throw new Error('[ERROR] 보너스 번호는 숫자로 입력해 주세요.'); + if (isNaN(bonusNumber)) throw new Error(ERROR_MESSAGES.BONUS_NUMBER_FORMAT); if (bonusNumber < 1 || bonusNumber > 45) - throw new Error('[ERROR] 보너스 번호는 1 ~ 45 범위로 입력해 주세요.'); + throw new Error(ERROR_MESSAGES.BONUS_NUMBER_RANGE); if (winningNumbers.includes(bonusNumber)) - throw new Error('[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.'); + throw new Error(ERROR_MESSAGES.BONUS_NUMBER_DUPLICATE); } } From 79ff6f68c68f44af5b25b67831e54343398b1c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 22:38:34 +0900 Subject: [PATCH 53/54] =?UTF-8?q?refactor(OutputHandler):=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/OutputHandler.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/view/OutputHandler.js b/src/view/OutputHandler.js index e97a0d732..ecb2c59d6 100644 --- a/src/view/OutputHandler.js +++ b/src/view/OutputHandler.js @@ -1,8 +1,9 @@ import { Console } from '@woowacourse/mission-utils'; +import { OUTPUT_MESSAGES } from '../constants/constants.js'; class OutputHandler { static printLottoCount(lottoCount) { - Console.print(`\n${lottoCount}개를 구매했습니다.`); + Console.print(OUTPUT_MESSAGES.PURCHASED_COUNT(lottoCount)); } static printLotto(lottos) { @@ -12,16 +13,16 @@ class OutputHandler { } static printStatistics(profitRate, winningRank) { - Console.print('\n당첨 통계'); - Console.print('---'); - Console.print(`3개 일치 (5,000원) - ${winningRank.match3}개`); - Console.print(`4개 일치 (50,000원) - ${winningRank.match4}개`); - Console.print(`5개 일치 (1,500,000원) - ${winningRank.match5}개`); + Console.print(OUTPUT_MESSAGES.STATISTICS_HEADER); + Console.print(OUTPUT_MESSAGES.STATISTICS_SEPARATOR); + Console.print(OUTPUT_MESSAGES.STATISTICS_MATCH3(winningRank.match3)); + Console.print(OUTPUT_MESSAGES.STATISTICS_MATCH4(winningRank.match4)); + Console.print(OUTPUT_MESSAGES.STATISTICS_MATCH5(winningRank.match5)); Console.print( - `5개 일치, 보너스 볼 일치 (30,000,000원) - ${winningRank.match5AndBonus}개` + OUTPUT_MESSAGES.STATISTICS_MATCH5_BONUS(winningRank.match5AndBonus) ); - Console.print(`6개 일치 (2,000,000,000원) - ${winningRank.match6}개`); - Console.print(`총 수익률은 ${profitRate}%입니다.`); + Console.print(OUTPUT_MESSAGES.STATISTICS_MATCH6(winningRank.match6)); + Console.print(OUTPUT_MESSAGES.PROFIT_RATE(profitRate)); } } From 6b74efb514d610f13b5d8f54886a6d1e1aa22af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EA=B6=8C=EC=A7=84?= Date: Mon, 3 Nov 2025 23:46:59 +0900 Subject: [PATCH 54/54] =?UTF-8?q?refactor:=20App.js=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/ApplicationTest.js | 2 +- src/{controller => }/App.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/{controller => }/App.js (81%) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index adae21381..b8a446383 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,4 +1,4 @@ -import App from '../src/controller/App.js'; +import App from '../src/App.js'; import { MissionUtils } from '@woowacourse/mission-utils'; const mockQuestions = (inputs) => { diff --git a/src/controller/App.js b/src/App.js similarity index 81% rename from src/controller/App.js rename to src/App.js index 7a516a85d..66c860693 100644 --- a/src/controller/App.js +++ b/src/App.js @@ -1,8 +1,8 @@ -import Lotto from '../lotto/Lotto.js'; -import LottoResult from '../lotto/LottoResult.js'; -import LottoUtils from '../lotto/LottoUtils.js'; -import InputHandler from '../view/InputHandler.js'; -import OutputHandler from '../view/OutputHandler.js'; +import Lotto from './lotto/Lotto.js'; +import LottoResult from './lotto/LottoResult.js'; +import LottoUtils from './lotto/LottoUtils.js'; +import InputHandler from './view/InputHandler.js'; +import OutputHandler from './view/OutputHandler.js'; class App { async run() {