From 2b967f50739419c02a046cf1fb2690e927bf1e7e Mon Sep 17 00:00:00 2001 From: gustn99 Date: Wed, 29 Oct 2025 22:52:04 +0900 Subject: [PATCH 01/46] =?UTF-8?q?docs:=20README=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index 15bb106b5..4c458672c 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ # javascript-lotto-precourse + +## 기능 개요 + +1. 로또 구매 +2. 자동 로또 생성 +3. 로또 추첨 +4. 당첨 결과 및 수익률 출력 + +## 세부 기능 목록 + +1. 구매 금액 입력 + - 빈 문자열 예외 + - 1000원 단위가 아니면 예외 + +
+ +2. 구매 금액을 기준으로 로또 번호 생성 +3. 구매한 로또 개수와 생성된 로또 번호 출력 + +
+ +4. 당첨 번호 입력 + - 빈 문자열 예외 + - 번호가 쉼표로 구분되지 않으면 예외 + - 쉼표로 구분된 번호가 6개가 아니면 예외 + - 1-45 사이 숫자가 아니면 예외 + - 중복 숫자가 있으면 예외 +5. 보너스 번호 입력 + - 빈 문자열 예외 + - 1-45 사이 숫자가 아니면 예외 + - 당첨 번호 중 하나라도 중복되면 예외 + +
+ +6. 랜덤 생성된 로또 번호들마다 당첨 여부 확인 +7. 총 수익률 계산 +8. 당첨 통계(1-5등 개수, 총 수익률) 출력 From a5b47ce182739e55c7775c6c10968784600c74c8 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 13:12:16 +0900 Subject: [PATCH 02/46] =?UTF-8?q?refactor:=20Lotto=EB=A5=BC=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=8F=B4=EB=8D=94=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/{ => domains}/Lotto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ => domains}/Lotto.js (89%) diff --git a/src/Lotto.js b/src/domains/Lotto.js similarity index 89% rename from src/Lotto.js rename to src/domains/Lotto.js index cb0b1527e..3de1b4141 100644 --- a/src/Lotto.js +++ b/src/domains/Lotto.js @@ -12,7 +12,7 @@ class Lotto { } } - // TODO: 추가 기능 구현 + format() {} } export default Lotto; From 4f8b839980bfaa1e08da6bb8245031de553bc7c6 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 13:28:59 +0900 Subject: [PATCH 03/46] =?UTF-8?q?feat(Lotto):=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=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__/ApplicationTest.js | 194 +++++++++++++++++------------------ __tests__/LottoTest.js | 3 +- src/domains/Lotto.js | 5 + 3 files changed, 103 insertions(+), 99 deletions(-) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 872380c9c..800e7ae3d 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,97 +1,97 @@ -import App from "../src/App.js"; -import { MissionUtils } from "@woowacourse/mission-utils"; - -const mockQuestions = (inputs) => { - MissionUtils.Console.readLineAsync = jest.fn(); - - MissionUtils.Console.readLineAsync.mockImplementation(() => { - const input = inputs.shift(); - - return Promise.resolve(input); - }); -}; - -const mockRandoms = (numbers) => { - MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); - numbers.reduce((acc, number) => { - return acc.mockReturnValueOnce(number); - }, MissionUtils.Random.pickUniqueNumbersInRange); -}; - -const getLogSpy = () => { - const logSpy = jest.spyOn(MissionUtils.Console, "print"); - logSpy.mockClear(); - return logSpy; -}; - -const runException = async (input) => { - // given - 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"]; - - mockRandoms([RANDOM_NUMBERS_TO_END]); - mockQuestions([input, ...INPUT_NUMBERS_TO_END]); - - // when - const app = new App(); - await app.run(); - - // then - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); -}; - -describe("로또 테스트", () => { - beforeEach(() => { - jest.restoreAllMocks(); - }); - - test("기능 테스트", async () => { - // given - const logSpy = getLogSpy(); - - mockRandoms([ - [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], - ]); - mockQuestions(["8000", "1,2,3,4,5,6", "7"]); - - // when - const app = new App(); - await app.run(); - - // 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%입니다.", - ]; - - logs.forEach((log) => { - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); - }); - }); - - test("예외 테스트", async () => { - await runException("1000j"); - }); -}); +// import App from "../src/App.js"; +// import { MissionUtils } from "@woowacourse/mission-utils"; + +// const mockQuestions = (inputs) => { +// MissionUtils.Console.readLineAsync = jest.fn(); + +// MissionUtils.Console.readLineAsync.mockImplementation(() => { +// const input = inputs.shift(); + +// return Promise.resolve(input); +// }); +// }; + +// const mockRandoms = (numbers) => { +// MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); +// numbers.reduce((acc, number) => { +// return acc.mockReturnValueOnce(number); +// }, MissionUtils.Random.pickUniqueNumbersInRange); +// }; + +// const getLogSpy = () => { +// const logSpy = jest.spyOn(MissionUtils.Console, "print"); +// logSpy.mockClear(); +// return logSpy; +// }; + +// const runException = async (input) => { +// // given +// 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"]; + +// mockRandoms([RANDOM_NUMBERS_TO_END]); +// mockQuestions([input, ...INPUT_NUMBERS_TO_END]); + +// // when +// const app = new App(); +// await app.run(); + +// // then +// expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); +// }; + +// describe("로또 테스트", () => { +// beforeEach(() => { +// jest.restoreAllMocks(); +// }); + +// test("기능 테스트", async () => { +// // given +// const logSpy = getLogSpy(); + +// mockRandoms([ +// [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], +// ]); +// mockQuestions(["8000", "1,2,3,4,5,6", "7"]); + +// // when +// const app = new App(); +// await app.run(); + +// // 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%입니다.", +// ]; + +// logs.forEach((log) => { +// expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); +// }); +// }); + +// test("예외 테스트", async () => { +// await runException("1000j"); +// }); +// }); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..9eaa2064d 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,4 +1,4 @@ -import Lotto from "../src/Lotto"; +import Lotto from "../src/domains/Lotto.js"; describe("로또 클래스 테스트", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { @@ -7,7 +7,6 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index 3de1b4141..caee473b4 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -10,6 +10,11 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } + + const numberSet = new Set(numbers); + if (numbers.length > numberSet.size) { + throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + } } format() {} From 2db839cf7726f6936cef43c75406d5d5c0c4dd50 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 13:29:14 +0900 Subject: [PATCH 04/46] =?UTF-8?q?feat(Lotto):=20=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=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 | 12 +++++++++++- src/domains/Lotto.js | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 9eaa2064d..5c0a72732 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -13,5 +13,15 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + }); + + test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR]"); + }); }); diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index caee473b4..4db939a1d 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -15,6 +15,14 @@ class Lotto { if (numbers.length > numberSet.size) { throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); } + + if (numbers.some((num) => num < 1)) { + throw new Error("[ERROR] 로또 번호는 1에서 45 사이의 숫자여야 합니다."); + } + + if (numbers.some((num) => num > 45)) { + throw new Error("[ERROR] 로또 번호는 1에서 45 사이의 숫자여야 합니다."); + } } format() {} From 6db0fac5d172a10796034ceab16460fcaa5fd4e6 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 13:39:44 +0900 Subject: [PATCH 05/46] =?UTF-8?q?feat(Lotto):=20format=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 47 ++++++++++++++++++++++++----------------- __tests__/LottosTest.js | 36 +++++++++++++++++++++++++++++++ src/domains/Lotto.js | 4 +++- 3 files changed, 67 insertions(+), 20 deletions(-) create mode 100644 __tests__/LottosTest.js diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 5c0a72732..f5f75523e 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,27 +1,36 @@ import Lotto from "../src/domains/Lotto.js"; -describe("로또 클래스 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); - }); +describe("Lotto 클래스", () => { + describe("생성자 테스트", () => { + test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6, 7]); + }).toThrow("[ERROR]"); + }); - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); - }); + test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 5]); + }).toThrow("[ERROR]"); + }); + + test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + }); - test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([0, 2, 3, 4, 5, 6]); - }).toThrow("[ERROR]"); + test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR]"); + }); }); - test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 46]); - }).toThrow("[ERROR]"); + describe("format 메서드 테스트", () => { + test("format 메서드 호출 시 출력 형식의 문자열을 반환한다.", () => { + const lotto = new Lotto([1, 2, 3, 4, 5, 6]); + expect(lotto.format()).toEqual("[1, 2, 3, 4, 5, 6]"); + }); }); }); diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js new file mode 100644 index 000000000..d30f8d69c --- /dev/null +++ b/__tests__/LottosTest.js @@ -0,0 +1,36 @@ +import Lotto from "../src/domains/Lotto.js"; + +describe("Lottos 클래스", () => { + describe("생성자 테스트", () => { + test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6, 7]); + }).toThrow("[ERROR]"); + }); + + test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 5]); + }).toThrow("[ERROR]"); + }); + + test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + }); + + test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR]"); + }); + }); + + describe("format 메서드 테스트", () => { + test("format 메서드 호출 시 출력 형식의 문자열을 반환한다.", () => { + const lotto = new Lotto([1, 2, 3, 4, 5, 6]); + expect(lotto.format()).toBe("[1, 2, 3, 4, 5, 6]"); + }); + }); +}); diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index 4db939a1d..ea2792004 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -25,7 +25,9 @@ class Lotto { } } - format() {} + format() { + return `[${this.#numbers.join(", ")}]`; + } } export default Lotto; From 98cf9c7aaa9ab681c00f7955b20a6f9fa2abf5c2 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 14:06:04 +0900 Subject: [PATCH 06/46] =?UTF-8?q?feat(Lottos):=20Lottos=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 50 ++++++++++++++++++++++-------------------- src/models/Lottos.js | 35 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 src/models/Lottos.js diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index f5f75523e..07908b035 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,36 +1,38 @@ import Lotto from "../src/domains/Lotto.js"; +import Lottos from "../src/models/Lottos.js"; -describe("Lotto 클래스", () => { +describe("Lottos 클래스", () => { describe("생성자 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); + test("입력 개수만큼의 lottos 배열을 생성한다.", () => { + const lottosInstance = new Lottos(3); + const lottos = lottosInstance._getLottos(); + expect(lottos).toHaveLength(3); }); - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); + test("lottos 배열은 Lotto 인스턴스로 구성된다.", () => { + const lottosInstance = new Lottos(3); + const lottos = lottosInstance._getLottos(); + lottos.forEach((lotto) => { + expect(lotto).toBeInstanceOf(Lotto); + }); }); - test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([0, 2, 3, 4, 5, 6]); - }).toThrow("[ERROR]"); + test("ranks(등수 통계)를 초기화한다.", () => { + const lottosInstance = new Lottos(3); + const ranks = lottosInstance._getRanks(); + expect(ranks).toEqual({ + "1st": 0, + "2nd": 0, + "3rd": 0, + "4th": 0, + "5th": 0, + }); }); - test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 46]); - }).toThrow("[ERROR]"); - }); - }); - - describe("format 메서드 테스트", () => { - test("format 메서드 호출 시 출력 형식의 문자열을 반환한다.", () => { - const lotto = new Lotto([1, 2, 3, 4, 5, 6]); - expect(lotto.format()).toEqual("[1, 2, 3, 4, 5, 6]"); + test("totalReturn(총 수익률)을 초기화한다.", () => { + const lottosInstance = new Lottos(3); + const totalReturn = lottosInstance._getTotalReturn(); + expect(totalReturn).toBe(0); }); }); }); diff --git a/src/models/Lottos.js b/src/models/Lottos.js new file mode 100644 index 000000000..dfb06d47c --- /dev/null +++ b/src/models/Lottos.js @@ -0,0 +1,35 @@ +import { Random } from "@woowacourse/mission-utils"; +import Lotto from "../domains/Lotto"; + +class Lottos { + #lottos; + #ranks; + #totalReturn; + + constructor(purchaseCount) { + this.#lottos = Array.from({ length: purchaseCount }, () => + this.#createLotto() + ); + this.#ranks = { "1st": 0, "2nd": 0, "3rd": 0, "4th": 0, "5th": 0 }; + this.#totalReturn = 0; + } + + #createLotto() { + const numbers = Random.pickUniqueNumbersInRange(1, 45, 6); + return new Lotto(numbers); + } + + _getLottos() { + return this.#lottos; + } + + _getRanks() { + return this.#ranks; + } + + _getTotalReturn() { + return this.#totalReturn; + } +} + +export default Lottos; From 75369b2c331bc1fc1290370601167229ec2fae80 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 14:24:06 +0900 Subject: [PATCH 07/46] =?UTF-8?q?feat(Lottos):=20format=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 25 ++++++++++++++++++++----- src/models/Lottos.js | 5 +++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 07908b035..13c44f86f 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,16 +1,18 @@ import Lotto from "../src/domains/Lotto.js"; import Lottos from "../src/models/Lottos.js"; +const PURCASE_COUNT = 3; + describe("Lottos 클래스", () => { describe("생성자 테스트", () => { test("입력 개수만큼의 lottos 배열을 생성한다.", () => { - const lottosInstance = new Lottos(3); + const lottosInstance = new Lottos(PURCASE_COUNT); const lottos = lottosInstance._getLottos(); - expect(lottos).toHaveLength(3); + expect(lottos).toHaveLength(PURCASE_COUNT); }); test("lottos 배열은 Lotto 인스턴스로 구성된다.", () => { - const lottosInstance = new Lottos(3); + const lottosInstance = new Lottos(PURCASE_COUNT); const lottos = lottosInstance._getLottos(); lottos.forEach((lotto) => { expect(lotto).toBeInstanceOf(Lotto); @@ -18,7 +20,7 @@ describe("Lottos 클래스", () => { }); test("ranks(등수 통계)를 초기화한다.", () => { - const lottosInstance = new Lottos(3); + const lottosInstance = new Lottos(PURCASE_COUNT); const ranks = lottosInstance._getRanks(); expect(ranks).toEqual({ "1st": 0, @@ -30,9 +32,22 @@ describe("Lottos 클래스", () => { }); test("totalReturn(총 수익률)을 초기화한다.", () => { - const lottosInstance = new Lottos(3); + const lottosInstance = new Lottos(PURCASE_COUNT); const totalReturn = lottosInstance._getTotalReturn(); expect(totalReturn).toBe(0); }); }); + + describe("format 메서드 테스트", () => { + test("모든 Lotto 객체의 format 결과를 줄바꿈으로 연결해 반환한다.", () => { + const lottosInstance = new Lottos(PURCASE_COUNT); + const formattedLottos = lottosInstance.format(); + const formattedLottoArray = formattedLottos.split("\n"); + + expect(formattedLottoArray).toHaveLength(PURCASE_COUNT); + formattedLottoArray.forEach((str) => { + expect(str).toMatch(/\[\d+(, \d+){5}\]/); + }); + }); + }); }); diff --git a/src/models/Lottos.js b/src/models/Lottos.js index dfb06d47c..d039e7a3e 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -19,6 +19,11 @@ class Lottos { return new Lotto(numbers); } + format() { + const formattedLottos = this.#lottos.map((lotto) => lotto.format()); + return formattedLottos.join("\n"); + } + _getLottos() { return this.#lottos; } From d13b7c16509af1b51fc2d5a29877ca546d52785a Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 14:27:31 +0900 Subject: [PATCH 08/46] =?UTF-8?q?refactor(LottoTest):=20format=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottosTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index d30f8d69c..b169cadc1 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -28,7 +28,7 @@ describe("Lottos 클래스", () => { }); describe("format 메서드 테스트", () => { - test("format 메서드 호출 시 출력 형식의 문자열을 반환한다.", () => { + test("배열을 문자열 형식으로 반환한다.", () => { const lotto = new Lotto([1, 2, 3, 4, 5, 6]); expect(lotto.format()).toBe("[1, 2, 3, 4, 5, 6]"); }); From 00d12e325abac85ed760968ed1b21be448146125 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 14:30:08 +0900 Subject: [PATCH 09/46] =?UTF-8?q?fix(test):=20Lotto,=20Lottos=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EC=84=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 57 +++++++++++++++-------------------------- __tests__/LottosTest.js | 55 +++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 13c44f86f..d4740abc5 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,53 +1,36 @@ import Lotto from "../src/domains/Lotto.js"; -import Lottos from "../src/models/Lottos.js"; -const PURCASE_COUNT = 3; - -describe("Lottos 클래스", () => { +describe("Lotto 클래스", () => { describe("생성자 테스트", () => { - test("입력 개수만큼의 lottos 배열을 생성한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); - const lottos = lottosInstance._getLottos(); - expect(lottos).toHaveLength(PURCASE_COUNT); + test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6, 7]); + }).toThrow("[ERROR]"); }); - test("lottos 배열은 Lotto 인스턴스로 구성된다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); - const lottos = lottosInstance._getLottos(); - lottos.forEach((lotto) => { - expect(lotto).toBeInstanceOf(Lotto); - }); + test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 5]); + }).toThrow("[ERROR]"); }); - test("ranks(등수 통계)를 초기화한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); - const ranks = lottosInstance._getRanks(); - expect(ranks).toEqual({ - "1st": 0, - "2nd": 0, - "3rd": 0, - "4th": 0, - "5th": 0, - }); + test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); }); - test("totalReturn(총 수익률)을 초기화한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); - const totalReturn = lottosInstance._getTotalReturn(); - expect(totalReturn).toBe(0); + test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR]"); }); }); describe("format 메서드 테스트", () => { - test("모든 Lotto 객체의 format 결과를 줄바꿈으로 연결해 반환한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); - const formattedLottos = lottosInstance.format(); - const formattedLottoArray = formattedLottos.split("\n"); - - expect(formattedLottoArray).toHaveLength(PURCASE_COUNT); - formattedLottoArray.forEach((str) => { - expect(str).toMatch(/\[\d+(, \d+){5}\]/); - }); + test("배열을 문자열 형식으로 반환한다.", () => { + const lotto = new Lotto([1, 2, 3, 4, 5, 6]); + expect(lotto.format()).toBe("[1, 2, 3, 4, 5, 6]"); }); }); }); diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index b169cadc1..13c44f86f 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -1,36 +1,53 @@ import Lotto from "../src/domains/Lotto.js"; +import Lottos from "../src/models/Lottos.js"; + +const PURCASE_COUNT = 3; describe("Lottos 클래스", () => { describe("생성자 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); + test("입력 개수만큼의 lottos 배열을 생성한다.", () => { + const lottosInstance = new Lottos(PURCASE_COUNT); + const lottos = lottosInstance._getLottos(); + expect(lottos).toHaveLength(PURCASE_COUNT); }); - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); + test("lottos 배열은 Lotto 인스턴스로 구성된다.", () => { + const lottosInstance = new Lottos(PURCASE_COUNT); + const lottos = lottosInstance._getLottos(); + lottos.forEach((lotto) => { + expect(lotto).toBeInstanceOf(Lotto); + }); }); - test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([0, 2, 3, 4, 5, 6]); - }).toThrow("[ERROR]"); + test("ranks(등수 통계)를 초기화한다.", () => { + const lottosInstance = new Lottos(PURCASE_COUNT); + const ranks = lottosInstance._getRanks(); + expect(ranks).toEqual({ + "1st": 0, + "2nd": 0, + "3rd": 0, + "4th": 0, + "5th": 0, + }); }); - test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 46]); - }).toThrow("[ERROR]"); + test("totalReturn(총 수익률)을 초기화한다.", () => { + const lottosInstance = new Lottos(PURCASE_COUNT); + const totalReturn = lottosInstance._getTotalReturn(); + expect(totalReturn).toBe(0); }); }); describe("format 메서드 테스트", () => { - test("배열을 문자열 형식으로 반환한다.", () => { - const lotto = new Lotto([1, 2, 3, 4, 5, 6]); - expect(lotto.format()).toBe("[1, 2, 3, 4, 5, 6]"); + test("모든 Lotto 객체의 format 결과를 줄바꿈으로 연결해 반환한다.", () => { + const lottosInstance = new Lottos(PURCASE_COUNT); + const formattedLottos = lottosInstance.format(); + const formattedLottoArray = formattedLottos.split("\n"); + + expect(formattedLottoArray).toHaveLength(PURCASE_COUNT); + formattedLottoArray.forEach((str) => { + expect(str).toMatch(/\[\d+(, \d+){5}\]/); + }); }); }); }); From 4e79321e78da39a61c493eacfaadec92fe51fb05 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 14:30:53 +0900 Subject: [PATCH 10/46] =?UTF-8?q?refactor(Lottos):=20lottos=20getter?= =?UTF-8?q?=EB=A5=BC=20=ED=8D=BC=EB=B8=94=EB=A6=AD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottosTest.js | 6 +++--- src/models/Lottos.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index 13c44f86f..62df09c19 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -7,13 +7,13 @@ describe("Lottos 클래스", () => { describe("생성자 테스트", () => { test("입력 개수만큼의 lottos 배열을 생성한다.", () => { const lottosInstance = new Lottos(PURCASE_COUNT); - const lottos = lottosInstance._getLottos(); + const lottos = lottosInstance.getLottos(); expect(lottos).toHaveLength(PURCASE_COUNT); }); test("lottos 배열은 Lotto 인스턴스로 구성된다.", () => { const lottosInstance = new Lottos(PURCASE_COUNT); - const lottos = lottosInstance._getLottos(); + const lottos = lottosInstance.getLottos(); lottos.forEach((lotto) => { expect(lotto).toBeInstanceOf(Lotto); }); @@ -21,7 +21,7 @@ describe("Lottos 클래스", () => { test("ranks(등수 통계)를 초기화한다.", () => { const lottosInstance = new Lottos(PURCASE_COUNT); - const ranks = lottosInstance._getRanks(); + const ranks = lottosInstance.getRanks(); expect(ranks).toEqual({ "1st": 0, "2nd": 0, diff --git a/src/models/Lottos.js b/src/models/Lottos.js index d039e7a3e..c68242873 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -24,7 +24,7 @@ class Lottos { return formattedLottos.join("\n"); } - _getLottos() { + getLottos() { return this.#lottos; } From c73bf5129c7580eaddb9c55472bb3cfe7e26a697 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 16:31:51 +0900 Subject: [PATCH 11/46] =?UTF-8?q?feat(Lottos):=20win=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 --- __tests__/LottosTest.js | 31 ++++++++++++++++++++++++++----- src/constants/rankToPrizeMap.js | 7 +++++++ src/models/Lottos.js | 14 ++++++++++---- 3 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 src/constants/rankToPrizeMap.js diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index 62df09c19..8dcf9f6c2 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -1,3 +1,4 @@ +import { RANK_TO_PRIZE_MAP } from "../src/constants/rankToPrizeMap.js"; import Lotto from "../src/domains/Lotto.js"; import Lottos from "../src/models/Lottos.js"; @@ -19,9 +20,9 @@ describe("Lottos 클래스", () => { }); }); - test("ranks(등수 통계)를 초기화한다.", () => { + test("등수 통계를 초기화한다.", () => { const lottosInstance = new Lottos(PURCASE_COUNT); - const ranks = lottosInstance.getRanks(); + const ranks = lottosInstance._getRanks(); expect(ranks).toEqual({ "1st": 0, "2nd": 0, @@ -31,10 +32,30 @@ describe("Lottos 클래스", () => { }); }); - test("totalReturn(총 수익률)을 초기화한다.", () => { + test("총 상금을 초기화한다.", () => { const lottosInstance = new Lottos(PURCASE_COUNT); - const totalReturn = lottosInstance._getTotalReturn(); - expect(totalReturn).toBe(0); + const totalPrize = lottosInstance._getTotalPrize(); + expect(totalPrize).toBe(0); + }); + }); + + describe("win 메서드 테스트", () => { + test("순위에 따라 등수 통계를 업데이트한다.", () => { + Object.keys(RANK_TO_PRIZE_MAP).forEach((rank) => { + const lottosInstance = new Lottos(PURCASE_COUNT); + lottosInstance.win(rank); + const ranks = lottosInstance._getRanks(); + expect(ranks[rank]).toBe(1); + }); + }); + + test("순위에 따라 총 상금을 업데이트한다.", () => { + Object.keys(RANK_TO_PRIZE_MAP).forEach((rank) => { + const lottosInstance = new Lottos(PURCASE_COUNT); + lottosInstance.win(rank); + const totalPrize = lottosInstance._getTotalPrize(); + expect(totalPrize).toBe(RANK_TO_PRIZE_MAP[rank]); + }); }); }); diff --git a/src/constants/rankToPrizeMap.js b/src/constants/rankToPrizeMap.js new file mode 100644 index 000000000..1d10b69db --- /dev/null +++ b/src/constants/rankToPrizeMap.js @@ -0,0 +1,7 @@ +export const RANK_TO_PRIZE_MAP = { + "1st": 2000000000, + "2nd": 30000000, + "3rd": 1500000, + "4th": 50000, + "5th": 5000, +}; diff --git a/src/models/Lottos.js b/src/models/Lottos.js index c68242873..77bad0172 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -1,17 +1,18 @@ import { Random } from "@woowacourse/mission-utils"; import Lotto from "../domains/Lotto"; +import { RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap"; class Lottos { #lottos; #ranks; - #totalReturn; + #totalPrize; constructor(purchaseCount) { this.#lottos = Array.from({ length: purchaseCount }, () => this.#createLotto() ); this.#ranks = { "1st": 0, "2nd": 0, "3rd": 0, "4th": 0, "5th": 0 }; - this.#totalReturn = 0; + this.#totalPrize = 0; } #createLotto() { @@ -19,6 +20,11 @@ class Lottos { return new Lotto(numbers); } + win(rank) { + this.#ranks[rank] += 1; + this.#totalPrize += RANK_TO_PRIZE_MAP[rank]; + } + format() { const formattedLottos = this.#lottos.map((lotto) => lotto.format()); return formattedLottos.join("\n"); @@ -32,8 +38,8 @@ class Lottos { return this.#ranks; } - _getTotalReturn() { - return this.#totalReturn; + _getTotalPrize() { + return this.#totalPrize; } } From 093f3166c731c6622041874607a7ce6279ff7caf Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 16:47:47 +0900 Subject: [PATCH 12/46] =?UTF-8?q?feat(Lottos):=20calculateTotalReturn=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottosTest.js | 17 +++++++++++++++++ src/models/Lottos.js | 8 ++++++++ 2 files changed, 25 insertions(+) diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index 8dcf9f6c2..ba66eadb3 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -71,4 +71,21 @@ describe("Lottos 클래스", () => { }); }); }); + + describe("calculateTotalReturn 메서드 테스트", () => { + test("상금을 수익률 형태로 변환해 반환한다.", () => { + const rank = "1st"; + const lottosInstance = new Lottos(PURCASE_COUNT); + lottosInstance.win(rank); + + const purchaseAmount = PURCASE_COUNT * 1000; + const expectedTotalPrize = RANK_TO_PRIZE_MAP[rank]; + const expectedTotalReturn = (expectedTotalPrize / purchaseAmount).toFixed( + 2 + ); + + const totalReturn = lottosInstance.calculateTotalReturn(); + expect(totalReturn).toBe(expectedTotalReturn); + }); + }); }); diff --git a/src/models/Lottos.js b/src/models/Lottos.js index 77bad0172..d32eab780 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -25,6 +25,14 @@ class Lottos { this.#totalPrize += RANK_TO_PRIZE_MAP[rank]; } + calculateTotalReturn() { + const purchaseCount = this.#lottos.length; + const purchaseAmount = purchaseCount * 1000; + + const totalReturn = (this.#totalPrize / purchaseAmount).toFixed(2); + return totalReturn; + } + format() { const formattedLottos = this.#lottos.map((lotto) => lotto.format()); return formattedLottos.join("\n"); From 6980ef8a094a1ed118247723a90edc0cf9533bbf Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 16:49:10 +0900 Subject: [PATCH 13/46] =?UTF-8?q?refactor(constant):=20=EA=B5=AC=EB=A7=A4?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84(1000)=EC=9D=84=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 --- __tests__/LottosTest.js | 25 +++++++++++++------------ src/constants/unit.js | 1 + src/models/Lottos.js | 3 ++- 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 src/constants/unit.js diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index ba66eadb3..81cd2ceb5 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -1,19 +1,20 @@ import { RANK_TO_PRIZE_MAP } from "../src/constants/rankToPrizeMap.js"; +import { PURCHASE_UNIT } from "../src/constants/unit.js"; import Lotto from "../src/domains/Lotto.js"; import Lottos from "../src/models/Lottos.js"; -const PURCASE_COUNT = 3; +const PURCHASE_COUNT = 3; describe("Lottos 클래스", () => { describe("생성자 테스트", () => { test("입력 개수만큼의 lottos 배열을 생성한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); const lottos = lottosInstance.getLottos(); - expect(lottos).toHaveLength(PURCASE_COUNT); + expect(lottos).toHaveLength(PURCHASE_COUNT); }); test("lottos 배열은 Lotto 인스턴스로 구성된다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); const lottos = lottosInstance.getLottos(); lottos.forEach((lotto) => { expect(lotto).toBeInstanceOf(Lotto); @@ -21,7 +22,7 @@ describe("Lottos 클래스", () => { }); test("등수 통계를 초기화한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); const ranks = lottosInstance._getRanks(); expect(ranks).toEqual({ "1st": 0, @@ -33,7 +34,7 @@ describe("Lottos 클래스", () => { }); test("총 상금을 초기화한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); const totalPrize = lottosInstance._getTotalPrize(); expect(totalPrize).toBe(0); }); @@ -42,7 +43,7 @@ describe("Lottos 클래스", () => { describe("win 메서드 테스트", () => { test("순위에 따라 등수 통계를 업데이트한다.", () => { Object.keys(RANK_TO_PRIZE_MAP).forEach((rank) => { - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); lottosInstance.win(rank); const ranks = lottosInstance._getRanks(); expect(ranks[rank]).toBe(1); @@ -51,7 +52,7 @@ describe("Lottos 클래스", () => { test("순위에 따라 총 상금을 업데이트한다.", () => { Object.keys(RANK_TO_PRIZE_MAP).forEach((rank) => { - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); lottosInstance.win(rank); const totalPrize = lottosInstance._getTotalPrize(); expect(totalPrize).toBe(RANK_TO_PRIZE_MAP[rank]); @@ -61,11 +62,11 @@ describe("Lottos 클래스", () => { describe("format 메서드 테스트", () => { test("모든 Lotto 객체의 format 결과를 줄바꿈으로 연결해 반환한다.", () => { - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); const formattedLottos = lottosInstance.format(); const formattedLottoArray = formattedLottos.split("\n"); - expect(formattedLottoArray).toHaveLength(PURCASE_COUNT); + expect(formattedLottoArray).toHaveLength(PURCHASE_COUNT); formattedLottoArray.forEach((str) => { expect(str).toMatch(/\[\d+(, \d+){5}\]/); }); @@ -75,10 +76,10 @@ describe("Lottos 클래스", () => { describe("calculateTotalReturn 메서드 테스트", () => { test("상금을 수익률 형태로 변환해 반환한다.", () => { const rank = "1st"; - const lottosInstance = new Lottos(PURCASE_COUNT); + const lottosInstance = new Lottos(PURCHASE_COUNT); lottosInstance.win(rank); - const purchaseAmount = PURCASE_COUNT * 1000; + const purchaseAmount = PURCHASE_COUNT * PURCHASE_UNIT; const expectedTotalPrize = RANK_TO_PRIZE_MAP[rank]; const expectedTotalReturn = (expectedTotalPrize / purchaseAmount).toFixed( 2 diff --git a/src/constants/unit.js b/src/constants/unit.js new file mode 100644 index 000000000..4da292a5f --- /dev/null +++ b/src/constants/unit.js @@ -0,0 +1 @@ +export const PURCHASE_UNIT = 1000; diff --git a/src/models/Lottos.js b/src/models/Lottos.js index d32eab780..9f8382eda 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -1,6 +1,7 @@ import { Random } from "@woowacourse/mission-utils"; import Lotto from "../domains/Lotto"; import { RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap"; +import { PURCHASE_UNIT } from "../constants/unit"; class Lottos { #lottos; @@ -27,7 +28,7 @@ class Lottos { calculateTotalReturn() { const purchaseCount = this.#lottos.length; - const purchaseAmount = purchaseCount * 1000; + const purchaseAmount = purchaseCount * PURCHASE_UNIT; const totalReturn = (this.#totalPrize / purchaseAmount).toFixed(2); return totalReturn; From da723291f2cbeae9b496651a3ac1677c36f468be Mon Sep 17 00:00:00 2001 From: gustn99 Date: Thu, 30 Oct 2025 16:56:02 +0900 Subject: [PATCH 14/46] =?UTF-8?q?refactor(constant):=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=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/lotto.js | 3 +++ src/domains/Lotto.js | 22 ++++++++++++++++------ src/models/Lottos.js | 12 ++++++++++-- 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/constants/lotto.js diff --git a/src/constants/lotto.js b/src/constants/lotto.js new file mode 100644 index 000000000..e2051631e --- /dev/null +++ b/src/constants/lotto.js @@ -0,0 +1,3 @@ +export const LOTTO_MIN_VALUE = 1; +export const LOTTO_MAX_VALUE = 45; +export const LOTTO_SIZE = 6; diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index ea2792004..ec41a1133 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -1,3 +1,9 @@ +import { + LOTTO_MAX_VALUE, + LOTTO_MIN_VALUE, + LOTTO_SIZE, +} from "../constants/lotto"; + class Lotto { #numbers; @@ -7,8 +13,8 @@ class Lotto { } #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + if (numbers.length !== LOTTO_SIZE) { + throw new Error(`[ERROR] 로또 번호는 ${LOTTO_SIZE}개여야 합니다.`); } const numberSet = new Set(numbers); @@ -16,12 +22,16 @@ class Lotto { throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); } - if (numbers.some((num) => num < 1)) { - throw new Error("[ERROR] 로또 번호는 1에서 45 사이의 숫자여야 합니다."); + if (numbers.some((num) => num < LOTTO_MIN_VALUE)) { + throw new Error( + `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ); } - if (numbers.some((num) => num > 45)) { - throw new Error("[ERROR] 로또 번호는 1에서 45 사이의 숫자여야 합니다."); + if (numbers.some((num) => num > LOTTO_MAX_VALUE)) { + throw new Error( + `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ); } } diff --git a/src/models/Lottos.js b/src/models/Lottos.js index 9f8382eda..76160c154 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -1,7 +1,12 @@ import { Random } from "@woowacourse/mission-utils"; -import Lotto from "../domains/Lotto"; import { RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap"; import { PURCHASE_UNIT } from "../constants/unit"; +import { + LOTTO_MAX_VALUE, + LOTTO_MIN_VALUE, + LOTTO_SIZE, +} from "../constants/lotto"; +import Lotto from "../domains/Lotto"; class Lottos { #lottos; @@ -17,7 +22,10 @@ class Lottos { } #createLotto() { - const numbers = Random.pickUniqueNumbersInRange(1, 45, 6); + const start = LOTTO_MIN_VALUE; + const end = LOTTO_MAX_VALUE; + const size = LOTTO_SIZE; + const numbers = Random.pickUniqueNumbersInRange(start, end, size); return new Lotto(numbers); } From 220983dceca8d18f48e3545b2cf030a30da3296e Mon Sep 17 00:00:00 2001 From: gustn99 Date: Fri, 31 Oct 2025 15:12:53 +0900 Subject: [PATCH 15/46] =?UTF-8?q?feat(DrawnNumbers):=20DrawnNumbers=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 11 +++++++++++ __tests__/LottoTest.js | 12 ++++++++++++ src/domains/Lotto.js | 4 ++++ src/models/DrawnNumbers.js | 20 ++++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 __tests__/DrawnNumbersTest.js create mode 100644 src/models/DrawnNumbers.js diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js new file mode 100644 index 000000000..108a7fb76 --- /dev/null +++ b/__tests__/DrawnNumbersTest.js @@ -0,0 +1,11 @@ +import DrawnNumbers from "../src/models/DrawnNumbers"; + +describe("DrawnNumbers 클래스", () => { + describe("생성자 테스트", () => { + test("보너스 번호가 이미 당첨 번호에 포함되어 있으면 예외를 발생한다.", () => { + expect(() => { + new DrawnNumbers([1, 2, 3, 4, 5, 6], 1); + }).toThrow("[ERROR]"); + }); + }); +}); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index d4740abc5..90f5b4de9 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -33,4 +33,16 @@ describe("Lotto 클래스", () => { expect(lotto.format()).toBe("[1, 2, 3, 4, 5, 6]"); }); }); + + describe("includes 메서드 테스트", () => { + test("로또 인스턴스에 인자값이 포함되어 있으면 true를 반환한다.", () => { + const lotto = new Lotto([1, 2, 3, 4, 5, 6]); + expect(lotto.includes(1)).toBe(true); + }); + + test("로또 인스턴스에 인자값이 포함되어 있지 않으면 false를 반환한다.", () => { + const lotto = new Lotto([1, 2, 3, 4, 5, 6]); + expect(lotto.includes(7)).toBe(false); + }); + }); }); diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index ec41a1133..439e9b7b5 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -38,6 +38,10 @@ class Lotto { format() { return `[${this.#numbers.join(", ")}]`; } + + includes(num) { + return this.#numbers.includes(num); + } } export default Lotto; diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js new file mode 100644 index 000000000..bbc6f2818 --- /dev/null +++ b/src/models/DrawnNumbers.js @@ -0,0 +1,20 @@ +import Lotto from "../domains/Lotto"; + +class DrawnNumbers { + #winningNumbers; + #bonusNumber; + + constructor(winningNumbers, bonusNumber) { + this.#winningNumbers = new Lotto(winningNumbers); + this.#validateBonusNumber(bonusNumber); + this.#bonusNumber = bonusNumber; + } + + #validateBonusNumber(bonusNumber) { + if (this.#winningNumbers.includes(bonusNumber)) { + throw new Error("[ERROR] 이미 당첨 번호에 포함된 번호입니다."); + } + } +} + +export default DrawnNumbers; From 31a54e94dcf154da56f65b0fa5b226ac9b46bd07 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Fri, 31 Oct 2025 15:35:08 +0900 Subject: [PATCH 16/46] =?UTF-8?q?refactor(DrawnNumbers):=20=EB=8B=B9?= =?UTF-8?q?=EC=B2=A8=20=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B3=80=ED=99=98=EB=8F=84=20DrawnNumbers=EC=9D=98=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=EC=9C=BC=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 --- __tests__/DrawnNumbersTest.js | 46 +++++++++++++++++++++++++++++++-- src/models/DrawnNumbers.js | 48 +++++++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 108a7fb76..35e7ea155 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -2,9 +2,51 @@ import DrawnNumbers from "../src/models/DrawnNumbers"; describe("DrawnNumbers 클래스", () => { describe("생성자 테스트", () => { - test("보너스 번호가 이미 당첨 번호에 포함되어 있으면 예외를 발생한다.", () => { + test("당첨 번호 입력이 없으면 예외가 발생한다.", () => { expect(() => { - new DrawnNumbers([1, 2, 3, 4, 5, 6], 1); + new DrawnNumbers("", "1"); + }).toThrow("[ERROR]"); + }); + + test("당첨 번호 입력이 공백이면 예외가 발생한다.", () => { + expect(() => { + new DrawnNumbers(" ", "1"); + }).toThrow("[ERROR]"); + }); + + test("당첨 번호가 쉼표 외 구분자로 분리되어 있으면 예외가 발생한다.", () => { + expect(() => { + new DrawnNumbers("1. 2. 3. 4. 5. 6", "1"); + }).toThrow("[ERROR]"); + }); + + test("보너스 번호 입력이 없으면 예외가 발생한다.", () => { + expect(() => { + new DrawnNumbers("1, 2, 3, 4, 5, 6", ""); + }).toThrow("[ERROR]"); + }); + + test("보너스 번호 입력이 공백이면 예외가 발생한다.", () => { + expect(() => { + new DrawnNumbers("1, 2, 3, 4, 5, 6", " "); + }).toThrow("[ERROR]"); + }); + + test("보너스 번호가 1보다 작으면 예외가 발생한다.", () => { + expect(() => { + new DrawnNumbers("1, 2, 3, 4, 5, 6", "0"); + }).toThrow("[ERROR]"); + }); + + test("보너스 번호가 45보다 크면 예외가 발생한다.", () => { + expect(() => { + new DrawnNumbers("1, 2, 3, 4, 5, 6", "46"); + }).toThrow("[ERROR]"); + }); + + test("보너스 번호가 이미 당첨 번호에 포함되어 있으면 예외가 발생한다.", () => { + expect(() => { + new DrawnNumbers("1, 2, 3, 4, 5, 6", "1"); }).toThrow("[ERROR]"); }); }); diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index bbc6f2818..0f7b3cc17 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -1,16 +1,54 @@ +import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE } from "../constants/lotto"; import Lotto from "../domains/Lotto"; class DrawnNumbers { #winningNumbers; #bonusNumber; - constructor(winningNumbers, bonusNumber) { - this.#winningNumbers = new Lotto(winningNumbers); - this.#validateBonusNumber(bonusNumber); - this.#bonusNumber = bonusNumber; + constructor(numbersString, bonusNumberString) { + this.#winningNumbers = this.#createWinningNumbers(numbersString); + this.#validateBonusNumber(bonusNumberString); + this.#bonusNumber = Number(bonusNumberString); } - #validateBonusNumber(bonusNumber) { + #createWinningNumbers(numbersString) { + this.#validateWinningNumbers(numbersString); + const winningNumberArray = numbersString.split(",").map(Number); + return new Lotto(winningNumberArray); + } + + #validateWinningNumbers(numbersString) { + const trimmedString = numbersString.trim(); + if (trimmedString === "") { + throw new Error("[ERROR] 당첨 번호를 입력해 주세요."); + } + + const format = /^\s*\d+(\s*,\s*\d+)*\s*$/; + if (!format.test(trimmedString)) { + throw new Error("[ERROR] 당첨 번호는 쉼표로 구분되어야 합니다."); + } + } + + #validateBonusNumber(bonusNumberString) { + const trimmedString = bonusNumberString.trim(); + if (trimmedString === "") { + throw new Error("[ERROR] 보너스 번호를 입력해 주세요."); + } + + const bonusNumber = Number(trimmedString); + + if (bonusNumber < LOTTO_MIN_VALUE) { + throw new Error( + `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ); + } + + if (bonusNumber > LOTTO_MAX_VALUE) { + throw new Error( + `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ); + } + if (this.#winningNumbers.includes(bonusNumber)) { throw new Error("[ERROR] 이미 당첨 번호에 포함된 번호입니다."); } From bbe33427bf9f7ed2007eae513e4ac03e4fcc6fe8 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Fri, 31 Oct 2025 15:53:23 +0900 Subject: [PATCH 17/46] =?UTF-8?q?feat(DrawnNumbers):=20matchCount=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 15 +++++++++++++++ src/domains/Lotto.js | 4 ++++ src/models/DrawnNumbers.js | 9 +++++++++ 3 files changed, 28 insertions(+) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 35e7ea155..b6e5f7a4d 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -1,3 +1,4 @@ +import Lotto from "../src/domains/Lotto"; import DrawnNumbers from "../src/models/DrawnNumbers"; describe("DrawnNumbers 클래스", () => { @@ -50,4 +51,18 @@ describe("DrawnNumbers 클래스", () => { }).toThrow("[ERROR]"); }); }); + + describe("matchCount 메서드 테스트", () => { + test("당첨 번호에 포함된 로또 번호 개수를 반환한다.", () => { + const lottoInstance = new Lotto([1, 3, 5, 7, 9, 11]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.matchCount(lottoInstance)).toBe(3); + }); + + test("당첨 번호에 포함된 로또 번호가 없으면 0을 반환한다.", () => { + const lottoInstance = new Lotto([7, 8, 9, 10, 11, 12]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.matchCount(lottoInstance)).toBe(0); + }); + }); }); diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index 439e9b7b5..f618acdaf 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -35,6 +35,10 @@ class Lotto { } } + getNumbers() { + return this.#numbers; + } + format() { return `[${this.#numbers.join(", ")}]`; } diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index 0f7b3cc17..bce4adb72 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -11,6 +11,15 @@ class DrawnNumbers { this.#bonusNumber = Number(bonusNumberString); } + matchCount(lotto) { + const lottoNumbers = lotto.getNumbers(); + const count = lottoNumbers.reduce( + (total, num) => (this.#winningNumbers.includes(num) ? total + 1 : total), + 0 + ); + return count; + } + #createWinningNumbers(numbersString) { this.#validateWinningNumbers(numbersString); const winningNumberArray = numbersString.split(",").map(Number); From 4df638eddc8c5b43278b081b95a5051f18a813aa Mon Sep 17 00:00:00 2001 From: gustn99 Date: Fri, 31 Oct 2025 16:09:06 +0900 Subject: [PATCH 18/46] =?UTF-8?q?refactor(DrawnNumbers):=20matchCount=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20Lotto=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=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 --- __tests__/DrawnNumbersTest.js | 14 -------------- __tests__/LottoTest.js | 14 ++++++++++++++ src/domains/Lotto.js | 11 +++++++---- src/models/DrawnNumbers.js | 9 --------- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index b6e5f7a4d..18d1e6612 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -51,18 +51,4 @@ describe("DrawnNumbers 클래스", () => { }).toThrow("[ERROR]"); }); }); - - describe("matchCount 메서드 테스트", () => { - test("당첨 번호에 포함된 로또 번호 개수를 반환한다.", () => { - const lottoInstance = new Lotto([1, 3, 5, 7, 9, 11]); - const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.matchCount(lottoInstance)).toBe(3); - }); - - test("당첨 번호에 포함된 로또 번호가 없으면 0을 반환한다.", () => { - const lottoInstance = new Lotto([7, 8, 9, 10, 11, 12]); - const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.matchCount(lottoInstance)).toBe(0); - }); - }); }); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 90f5b4de9..8cdc0e895 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -34,6 +34,20 @@ describe("Lotto 클래스", () => { }); }); + describe("compare 메서드 테스트", () => { + test("두 로또 인스턴스 사이의 공통 원소 개수를 반환한다.", () => { + const lotto1 = new Lotto([1, 3, 5, 7, 9, 11]); + const lotto2 = new Lotto([1, 2, 3, 4, 5, 6]); + expect(lotto2.compare(lotto1)).toBe(3); + }); + + test("두 로또 인스턴스 사이에 공통 원소가 없으면 0을 반환한다.", () => { + const lotto1 = new Lotto([1, 3, 5, 7, 9, 11]); + const lotto2 = new Lotto([2, 4, 6, 8, 10, 12]); + expect(lotto2.compare(lotto1)).toBe(0); + }); + }); + describe("includes 메서드 테스트", () => { test("로또 인스턴스에 인자값이 포함되어 있으면 true를 반환한다.", () => { const lotto = new Lotto([1, 2, 3, 4, 5, 6]); diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index f618acdaf..ecfc467a6 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -35,14 +35,17 @@ class Lotto { } } - getNumbers() { - return this.#numbers; - } - format() { return `[${this.#numbers.join(", ")}]`; } + compare(anotherLotto) { + return this.#numbers.reduce( + (total, num) => (anotherLotto.includes(num) ? total + 1 : total), + 0 + ); + } + includes(num) { return this.#numbers.includes(num); } diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index bce4adb72..0f7b3cc17 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -11,15 +11,6 @@ class DrawnNumbers { this.#bonusNumber = Number(bonusNumberString); } - matchCount(lotto) { - const lottoNumbers = lotto.getNumbers(); - const count = lottoNumbers.reduce( - (total, num) => (this.#winningNumbers.includes(num) ? total + 1 : total), - 0 - ); - return count; - } - #createWinningNumbers(numbersString) { this.#validateWinningNumbers(numbersString); const winningNumberArray = numbersString.split(",").map(Number); From c427abb02b3e30434a7aa7d3eb8608c01b014d72 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Fri, 31 Oct 2025 16:14:20 +0900 Subject: [PATCH 19/46] =?UTF-8?q?feat(LottoDrawer):=20LottoDrawer=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/LottoDrawer.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/models/LottoDrawer.js diff --git a/src/models/LottoDrawer.js b/src/models/LottoDrawer.js new file mode 100644 index 000000000..6bf4a535d --- /dev/null +++ b/src/models/LottoDrawer.js @@ -0,0 +1,13 @@ +import DrawnNumbers from "./DrawnNumbers"; + +class LottoDrawer { + #lottos; + #drawnNumbers; + + constructor(lottos, winningNumbers, bonusNumber) { + this.#lottos = lottos; + this.#drawnNumbers = new DrawnNumbers(winningNumbers, bonusNumber); + } +} + +export default LottoDrawer; From 06d2a931d85ca761071d372b76983ba46db44abb Mon Sep 17 00:00:00 2001 From: gustn99 Date: Sat, 1 Nov 2025 23:14:55 +0900 Subject: [PATCH 20/46] =?UTF-8?q?feat(DrawnNumbers):=20calculateRank=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 38 +++++++++++++++++++++++++++++++++++ src/models/DrawnNumbers.js | 12 +++++++++++ 2 files changed, 50 insertions(+) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 18d1e6612..1dc2db0b0 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -51,4 +51,42 @@ describe("DrawnNumbers 클래스", () => { }).toThrow("[ERROR]"); }); }); + + describe("calculateRank 메서드 테스트", () => { + test("당첨 번호와 로또 번호가 6개 모두 일치하는 경우 1등을 반환한다.", () => { + const lottoInstance = new Lotto([1, 2, 3, 4, 5, 6]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("1st"); + }); + + test("당첨 번호와 로또 번호가 5개 일치하고, 로또 번호에 보너스 번호가 포함되어 있는 경우 2등을 반환한다.", () => { + const lottoInstance = new Lotto([1, 2, 3, 4, 5, 7]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("2nd"); + }); + + test("당첨 번호와 로또 번호가 5개 일치하는 경우 3등을 반환한다.", () => { + const lottoInstance = new Lotto([1, 2, 3, 4, 5, 8]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("3rd"); + }); + + test("당첨 번호와 로또 번호가 4개 일치하는 경우 4등을 반환한다.", () => { + const lottoInstance = new Lotto([1, 2, 3, 4, 7, 8]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("4th"); + }); + + test("당첨 번호와 로또 번호가 3개 일치하는 경우 5등을 반환한다.", () => { + const lottoInstance = new Lotto([1, 2, 3, 7, 8, 9]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("5th"); + }); + + test("당첨 번호와 로또 번호가 2개 이하 일치하는 경우 기타를 반환한다.", () => { + const lottoInstance = new Lotto([1, 2, 7, 8, 9, 10]); + const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("etc"); + }); + }); }); diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index 0f7b3cc17..9d72a2655 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -11,6 +11,18 @@ class DrawnNumbers { this.#bonusNumber = Number(bonusNumberString); } + calculateRank(lotto) { + const matchCount = lotto.compare(this.#winningNumbers); + const hasBonus = lotto.includes(this.#bonusNumber); + + if (matchCount === 6) return "1st"; + if (matchCount === 5 && hasBonus) return "2nd"; + if (matchCount === 5) return "3rd"; + if (matchCount === 4) return "4th"; + if (matchCount === 3) return "5th"; + return "etc"; + } + #createWinningNumbers(numbersString) { this.#validateWinningNumbers(numbersString); const winningNumberArray = numbersString.split(",").map(Number); From 6effb0645b4b55a3514613121bf4c8d3ec131388 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Sat, 1 Nov 2025 23:37:33 +0900 Subject: [PATCH 21/46] =?UTF-8?q?refactor(constants):=20=EB=93=B1=EC=88=98?= =?UTF-8?q?=20=ED=91=9C=EA=B8=B0=EB=A5=BC=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 25 +++++++++++++++++++------ __tests__/LottosTest.js | 22 ++-------------------- src/constants/rankToPrizeMap.js | 20 +++++++++++++++----- src/models/Lottos.js | 8 ++++++-- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 1dc2db0b0..b6185645c 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -1,3 +1,4 @@ +import { RANK } from "../src/constants/rankToPrizeMap"; import Lotto from "../src/domains/Lotto"; import DrawnNumbers from "../src/models/DrawnNumbers"; @@ -56,37 +57,49 @@ describe("DrawnNumbers 클래스", () => { test("당첨 번호와 로또 번호가 6개 모두 일치하는 경우 1등을 반환한다.", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 5, 6]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("1st"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( + RANK["1ST"] + ); }); test("당첨 번호와 로또 번호가 5개 일치하고, 로또 번호에 보너스 번호가 포함되어 있는 경우 2등을 반환한다.", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 5, 7]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("2nd"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( + RANK["2ND"] + ); }); test("당첨 번호와 로또 번호가 5개 일치하는 경우 3등을 반환한다.", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 5, 8]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("3rd"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( + RANK["3RD"] + ); }); test("당첨 번호와 로또 번호가 4개 일치하는 경우 4등을 반환한다.", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 7, 8]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("4th"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( + RANK["4TH"] + ); }); test("당첨 번호와 로또 번호가 3개 일치하는 경우 5등을 반환한다.", () => { const lottoInstance = new Lotto([1, 2, 3, 7, 8, 9]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("5th"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( + RANK["5TH"] + ); }); test("당첨 번호와 로또 번호가 2개 이하 일치하는 경우 기타를 반환한다.", () => { const lottoInstance = new Lotto([1, 2, 7, 8, 9, 10]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe("etc"); + expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( + RANK["ETC"] + ); }); }); }); diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index 81cd2ceb5..a50198f2e 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -1,4 +1,4 @@ -import { RANK_TO_PRIZE_MAP } from "../src/constants/rankToPrizeMap.js"; +import { RANK, RANK_TO_PRIZE_MAP } from "../src/constants/rankToPrizeMap.js"; import { PURCHASE_UNIT } from "../src/constants/unit.js"; import Lotto from "../src/domains/Lotto.js"; import Lottos from "../src/models/Lottos.js"; @@ -20,24 +20,6 @@ describe("Lottos 클래스", () => { expect(lotto).toBeInstanceOf(Lotto); }); }); - - test("등수 통계를 초기화한다.", () => { - const lottosInstance = new Lottos(PURCHASE_COUNT); - const ranks = lottosInstance._getRanks(); - expect(ranks).toEqual({ - "1st": 0, - "2nd": 0, - "3rd": 0, - "4th": 0, - "5th": 0, - }); - }); - - test("총 상금을 초기화한다.", () => { - const lottosInstance = new Lottos(PURCHASE_COUNT); - const totalPrize = lottosInstance._getTotalPrize(); - expect(totalPrize).toBe(0); - }); }); describe("win 메서드 테스트", () => { @@ -75,7 +57,7 @@ describe("Lottos 클래스", () => { describe("calculateTotalReturn 메서드 테스트", () => { test("상금을 수익률 형태로 변환해 반환한다.", () => { - const rank = "1st"; + const rank = RANK["1ST"]; const lottosInstance = new Lottos(PURCHASE_COUNT); lottosInstance.win(rank); diff --git a/src/constants/rankToPrizeMap.js b/src/constants/rankToPrizeMap.js index 1d10b69db..04a7195b3 100644 --- a/src/constants/rankToPrizeMap.js +++ b/src/constants/rankToPrizeMap.js @@ -1,7 +1,17 @@ +export const RANK = { + "1ST": "1st", + "2ND": "2nd", + "3RD": "3rd", + "4TH": "4th", + "5TH": "5th", + ETC: "etc", +}; + export const RANK_TO_PRIZE_MAP = { - "1st": 2000000000, - "2nd": 30000000, - "3rd": 1500000, - "4th": 50000, - "5th": 5000, + [RANK["1ST"]]: 2000000000, + [RANK["2ND"]]: 30000000, + [RANK["3RD"]]: 1500000, + [RANK["4TH"]]: 50000, + [RANK["5TH"]]: 5000, + [RANK["ETC"]]: 0, }; diff --git a/src/models/Lottos.js b/src/models/Lottos.js index 76160c154..dba14ae88 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -1,5 +1,5 @@ import { Random } from "@woowacourse/mission-utils"; -import { RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap"; +import { RANK, RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap"; import { PURCHASE_UNIT } from "../constants/unit"; import { LOTTO_MAX_VALUE, @@ -17,7 +17,7 @@ class Lottos { this.#lottos = Array.from({ length: purchaseCount }, () => this.#createLotto() ); - this.#ranks = { "1st": 0, "2nd": 0, "3rd": 0, "4th": 0, "5th": 0 }; + this.#ranks = this.#createRankCount(); this.#totalPrize = 0; } @@ -29,6 +29,10 @@ class Lottos { return new Lotto(numbers); } + #createRankCount() { + return Object.values(RANK).reduce((acc, cur) => ({ ...acc, [cur]: 0 }), {}); + } + win(rank) { this.#ranks[rank] += 1; this.#totalPrize += RANK_TO_PRIZE_MAP[rank]; From 7338f71f05855190d0243f00d2eb3b3ed39c0021 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Sat, 1 Nov 2025 23:41:05 +0900 Subject: [PATCH 22/46] =?UTF-8?q?refactor(LottosTest):=20=EC=A0=81?= =?UTF-8?q?=EC=A0=88=ED=95=9C=20object=20=EC=88=9C=ED=9A=8C=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottosTest.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index a50198f2e..e18f2e8f9 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -24,7 +24,7 @@ describe("Lottos 클래스", () => { describe("win 메서드 테스트", () => { test("순위에 따라 등수 통계를 업데이트한다.", () => { - Object.keys(RANK_TO_PRIZE_MAP).forEach((rank) => { + Object.values(RANK).forEach((rank) => { const lottosInstance = new Lottos(PURCHASE_COUNT); lottosInstance.win(rank); const ranks = lottosInstance._getRanks(); @@ -33,11 +33,11 @@ describe("Lottos 클래스", () => { }); test("순위에 따라 총 상금을 업데이트한다.", () => { - Object.keys(RANK_TO_PRIZE_MAP).forEach((rank) => { + Object.entries(RANK_TO_PRIZE_MAP).forEach(([rank, prize]) => { const lottosInstance = new Lottos(PURCHASE_COUNT); lottosInstance.win(rank); const totalPrize = lottosInstance._getTotalPrize(); - expect(totalPrize).toBe(RANK_TO_PRIZE_MAP[rank]); + expect(totalPrize).toBe(prize); }); }); }); @@ -57,18 +57,19 @@ describe("Lottos 클래스", () => { describe("calculateTotalReturn 메서드 테스트", () => { test("상금을 수익률 형태로 변환해 반환한다.", () => { - const rank = RANK["1ST"]; - const lottosInstance = new Lottos(PURCHASE_COUNT); - lottosInstance.win(rank); + Object.entries(RANK_TO_PRIZE_MAP).forEach(([rank, prize]) => { + const lottosInstance = new Lottos(PURCHASE_COUNT); + lottosInstance.win(rank); - const purchaseAmount = PURCHASE_COUNT * PURCHASE_UNIT; - const expectedTotalPrize = RANK_TO_PRIZE_MAP[rank]; - const expectedTotalReturn = (expectedTotalPrize / purchaseAmount).toFixed( - 2 - ); + const purchaseAmount = PURCHASE_COUNT * PURCHASE_UNIT; + const expectedTotalPrize = prize; + const expectedTotalReturn = ( + expectedTotalPrize / purchaseAmount + ).toFixed(2); - const totalReturn = lottosInstance.calculateTotalReturn(); - expect(totalReturn).toBe(expectedTotalReturn); + const totalReturn = lottosInstance.calculateTotalReturn(); + expect(totalReturn).toBe(expectedTotalReturn); + }); }); }); }); From 915aa9ea94815acbb1527609c474d51b07f0da56 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Sat, 1 Nov 2025 23:42:23 +0900 Subject: [PATCH 23/46] =?UTF-8?q?feat(LottoDrawer):=20LottoDrawer=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/LottoDrawer.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/models/LottoDrawer.js b/src/models/LottoDrawer.js index 6bf4a535d..96945b4f6 100644 --- a/src/models/LottoDrawer.js +++ b/src/models/LottoDrawer.js @@ -8,6 +8,14 @@ class LottoDrawer { this.#lottos = lottos; this.#drawnNumbers = new DrawnNumbers(winningNumbers, bonusNumber); } + + run() { + const lottoArray = this.#lottos.getLottos(); + lottoArray.forEach((lotto) => { + const rank = this.#drawnNumbers.calculateRank(lotto); + this.#lottos.win(rank); + }); + } } export default LottoDrawer; From 9cfdbcd178fbed4e21cf891ca9069843fa94220b Mon Sep 17 00:00:00 2001 From: gustn99 Date: Sat, 1 Nov 2025 23:49:09 +0900 Subject: [PATCH 24/46] =?UTF-8?q?feat(InputView):=20InputView=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/view/InputView.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/view/InputView.js diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 000000000..0ea587994 --- /dev/null +++ b/src/view/InputView.js @@ -0,0 +1,9 @@ +import { Console } from "@woowacourse/mission-utils"; + +class InputView { + async readLineAsync(question) { + return await Console.readLineAsync(`${question}\n`); + } +} + +export default InputView; From 4fb78bcc256143098aee129788cbe7e664207118 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Sat, 1 Nov 2025 23:50:50 +0900 Subject: [PATCH 25/46] =?UTF-8?q?feat(OutputView):=20OutputView=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/OutputView.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/view/OutputView.js diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 000000000..67ecb63d5 --- /dev/null +++ b/src/view/OutputView.js @@ -0,0 +1,9 @@ +import { Console } from "@woowacourse/mission-utils"; + +class OutputView { + print(value = "") { + Console.print(value); + } +} + +export default OutputView; From 4cbb0b8c64eecd10ce7d85e0e092c0e04f233f75 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Sat, 1 Nov 2025 23:52:47 +0900 Subject: [PATCH 26/46] =?UTF-8?q?feat(LottoController):=20LottoController?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/LottoController.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/controller/LottoController.js diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 000000000..d3bcb4706 --- /dev/null +++ b/src/controller/LottoController.js @@ -0,0 +1,14 @@ +import InputView from "../view/InputView"; +import OutputView from "../view/OutputView"; + +class LottoController { + #inputView; + #outputView; + + constructor() { + this.#inputView = new InputView(); + this.#outputView = new OutputView(); + } +} + +export default LottoController; From 5c4aa7b9a7c1370c85c64460b59a5904d0c24621 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 18:36:14 +0900 Subject: [PATCH 27/46] =?UTF-8?q?style:=20import=20=EC=8B=9C=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/Lotto.js | 2 +- src/models/DrawnNumbers.js | 4 ++-- src/models/LottoDrawer.js | 2 +- src/models/Lottos.js | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index ecfc467a6..1e92a3f03 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -2,7 +2,7 @@ import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE, LOTTO_SIZE, -} from "../constants/lotto"; +} from "../constants/lotto.js"; class Lotto { #numbers; diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index 9d72a2655..4daa22113 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -1,5 +1,5 @@ -import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE } from "../constants/lotto"; -import Lotto from "../domains/Lotto"; +import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE } from "../constants/lotto.js"; +import Lotto from "../domains/Lotto.js"; class DrawnNumbers { #winningNumbers; diff --git a/src/models/LottoDrawer.js b/src/models/LottoDrawer.js index 96945b4f6..9ca727b30 100644 --- a/src/models/LottoDrawer.js +++ b/src/models/LottoDrawer.js @@ -1,4 +1,4 @@ -import DrawnNumbers from "./DrawnNumbers"; +import DrawnNumbers from "./DrawnNumbers.js"; class LottoDrawer { #lottos; diff --git a/src/models/Lottos.js b/src/models/Lottos.js index dba14ae88..11ac51402 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -1,12 +1,12 @@ import { Random } from "@woowacourse/mission-utils"; -import { RANK, RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap"; -import { PURCHASE_UNIT } from "../constants/unit"; +import { RANK, RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap.js"; +import { PURCHASE_UNIT } from "../constants/unit.js"; import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE, LOTTO_SIZE, -} from "../constants/lotto"; -import Lotto from "../domains/Lotto"; +} from "../constants/lotto.js"; +import Lotto from "../domains/Lotto.js"; class Lottos { #lottos; From c190203e3080408aca5cfb867db72735913e3683 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 19:48:46 +0900 Subject: [PATCH 28/46] =?UTF-8?q?feat(Lottos):=20formatResult=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/rankToPrizeMap.js | 9 ++++++++- src/models/Lottos.js | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/constants/rankToPrizeMap.js b/src/constants/rankToPrizeMap.js index 04a7195b3..cfb705e48 100644 --- a/src/constants/rankToPrizeMap.js +++ b/src/constants/rankToPrizeMap.js @@ -13,5 +13,12 @@ export const RANK_TO_PRIZE_MAP = { [RANK["3RD"]]: 1500000, [RANK["4TH"]]: 50000, [RANK["5TH"]]: 5000, - [RANK["ETC"]]: 0, +}; + +export const RANK_TO_MATCH_STRING_MAP = { + [RANK["1ST"]]: "6개 일치", + [RANK["2ND"]]: "5개 일치, 보너스 볼 일치", + [RANK["3RD"]]: "5개 일치", + [RANK["4TH"]]: "4개 일치", + [RANK["5TH"]]: "3개 일치", }; diff --git a/src/models/Lottos.js b/src/models/Lottos.js index 11ac51402..90cf4e02c 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -1,5 +1,9 @@ import { Random } from "@woowacourse/mission-utils"; -import { RANK, RANK_TO_PRIZE_MAP } from "../constants/rankToPrizeMap.js"; +import { + RANK, + RANK_TO_MATCH_STRING_MAP, + RANK_TO_PRIZE_MAP, +} from "../constants/rankToPrizeMap.js"; import { PURCHASE_UNIT } from "../constants/unit.js"; import { LOTTO_MAX_VALUE, @@ -46,6 +50,17 @@ class Lottos { return totalReturn; } + formatResult() { + return Object.entries(RANK_TO_PRIZE_MAP) + .map( + ([rank, prize]) => + `${RANK_TO_MATCH_STRING_MAP[rank]} (${prize.toLocaleString()}원) - ${ + this.#ranks[rank] + }개` + ) + .join("\n"); + } + format() { const formattedLottos = this.#lottos.map((lotto) => lotto.format()); return formattedLottos.join("\n"); From ec90ebb92bd93fe8649718861c4f68a62bbffbf3 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 19:50:23 +0900 Subject: [PATCH 29/46] =?UTF-8?q?fix(Lottos):=20=EC=88=98=EC=9D=B5?= =?UTF-8?q?=EB=A5=A0=EC=9D=84=20=EC=86=8C=EC=88=AB=EC=A0=90=20=EB=91=98?= =?UTF-8?q?=EC=A7=B8=20=EC=9E=90=EB=A6=AC=EC=97=90=EC=84=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=AC=EB=A6=BC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottosTest.js | 2 +- src/models/Lottos.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index e18f2e8f9..143aa411c 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -65,7 +65,7 @@ describe("Lottos 클래스", () => { const expectedTotalPrize = prize; const expectedTotalReturn = ( expectedTotalPrize / purchaseAmount - ).toFixed(2); + ).toFixed(1); const totalReturn = lottosInstance.calculateTotalReturn(); expect(totalReturn).toBe(expectedTotalReturn); diff --git a/src/models/Lottos.js b/src/models/Lottos.js index 90cf4e02c..daf63235f 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -46,7 +46,7 @@ class Lottos { const purchaseCount = this.#lottos.length; const purchaseAmount = purchaseCount * PURCHASE_UNIT; - const totalReturn = (this.#totalPrize / purchaseAmount).toFixed(2); + const totalReturn = (this.#totalPrize / purchaseAmount).toFixed(1); return totalReturn; } From 55667abf458861c3b1426f8ac73df0dedc0086e6 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 19:51:24 +0900 Subject: [PATCH 30/46] =?UTF-8?q?refactor(DrawnNumbers):=20etc=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 8 -------- src/models/DrawnNumbers.js | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index b6185645c..362445466 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -93,13 +93,5 @@ describe("DrawnNumbers 클래스", () => { RANK["5TH"] ); }); - - test("당첨 번호와 로또 번호가 2개 이하 일치하는 경우 기타를 반환한다.", () => { - const lottoInstance = new Lotto([1, 2, 7, 8, 9, 10]); - const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); - expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( - RANK["ETC"] - ); - }); }); }); diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index 4daa22113..bd8509ddf 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -20,7 +20,7 @@ class DrawnNumbers { if (matchCount === 5) return "3rd"; if (matchCount === 4) return "4th"; if (matchCount === 3) return "5th"; - return "etc"; + return undefined; } #createWinningNumbers(numbersString) { From c4364783294bd2f238d36bf75083baa9519d618d Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 19:52:28 +0900 Subject: [PATCH 31/46] =?UTF-8?q?refactor(LottoDrawer):=20lottos=EB=A5=BC?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/LottoDrawer.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/models/LottoDrawer.js b/src/models/LottoDrawer.js index 9ca727b30..2629f4f5a 100644 --- a/src/models/LottoDrawer.js +++ b/src/models/LottoDrawer.js @@ -1,19 +1,17 @@ import DrawnNumbers from "./DrawnNumbers.js"; class LottoDrawer { - #lottos; #drawnNumbers; - constructor(lottos, winningNumbers, bonusNumber) { - this.#lottos = lottos; + constructor(winningNumbers, bonusNumber) { this.#drawnNumbers = new DrawnNumbers(winningNumbers, bonusNumber); } - run() { - const lottoArray = this.#lottos.getLottos(); + run(lottos) { + const lottoArray = lottos.getLottos(); lottoArray.forEach((lotto) => { const rank = this.#drawnNumbers.calculateRank(lotto); - this.#lottos.win(rank); + if (rank) lottos.win(rank); }); } } From aa57a7c1118bbcf8710c189e85f9622fcf6427ac Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 19:57:10 +0900 Subject: [PATCH 32/46] =?UTF-8?q?feat(LottoController):=20LottoController?= =?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/App.js | 7 ++++- src/controller/LottoController.js | 50 +++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/App.js b/src/App.js index 091aa0a5d..68f48ef5a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import LottoController from "./controller/LottoController.js"; + class App { - async run() {} + async run() { + const lottoController = new LottoController(); + lottoController.run(); + } } export default App; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index d3bcb4706..a2c6efafd 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,5 +1,8 @@ -import InputView from "../view/InputView"; -import OutputView from "../view/OutputView"; +import { PURCHASE_UNIT } from "../constants/unit.js"; +import LottoDrawer from "../models/LottoDrawer.js"; +import Lottos from "../models/Lottos.js"; +import InputView from "../view/InputView.js"; +import OutputView from "../view/OutputView.js"; class LottoController { #inputView; @@ -9,6 +12,49 @@ class LottoController { this.#inputView = new InputView(); this.#outputView = new OutputView(); } + + async run() { + const purchaseAmount = await this.#inputView.readLineAsync( + "구입금액을 입력해 주세요." + ); + this.#validatePurchaseAmount(purchaseAmount); + + const purchaseCount = purchaseAmount / PURCHASE_UNIT; + const lottos = new Lottos(purchaseCount); + this.#outputView.print(`${purchaseAmount}개를 구매했습니다.`); + this.#printLottos(lottos); + + const winningNumberString = await this.#inputView.readLineAsync( + "당첨 번호를 입력해 주세요." + ); + const bonusNumberString = await this.#inputView.readLineAsync( + "보너스 번호를 입력해 주세요." + ); + + const lottoDrawer = new LottoDrawer(winningNumberString, bonusNumberString); + lottoDrawer.run(lottos); + this.#printResult(lottos); + } + + #validatePurchaseAmount(purchaseAmount) { + if (purchaseAmount % PURCHASE_UNIT !== 0) { + throw new Error("[ERROR] 구입 금액은 1000원 단위로 입력해야 합니다."); + } + } + + #printLottos(lottos) { + const formattedLottos = lottos.format(); + this.#outputView.print(formattedLottos); + } + + #printResult(lottos) { + const formattedRanks = lottos.formatResult(); + const totalReturn = lottos.calculateTotalReturn(); + const formattedTotalReturn = `총 수익률은 ${totalReturn}%입니다.`; + this.#outputView.print("당첨 통계"); + this.#outputView.print(formattedRanks); + this.#outputView.print(formattedTotalReturn); + } } export default LottoController; From 045ee9e2bdf0b16d95a441f62a8f94905b49373d Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 20:18:45 +0900 Subject: [PATCH 33/46] =?UTF-8?q?feat(Formatter):=20Formatter=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 7 ------ __tests__/LottosTest.js | 15 +----------- src/controller/LottoController.js | 23 +++++++++++++++---- src/domains/Lotto.js | 8 +++---- src/models/Lottos.js | 18 +-------------- src/utils/Formatter.js | 38 +++++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 47 deletions(-) create mode 100644 src/utils/Formatter.js diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 8cdc0e895..0097230ce 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -27,13 +27,6 @@ describe("Lotto 클래스", () => { }); }); - describe("format 메서드 테스트", () => { - test("배열을 문자열 형식으로 반환한다.", () => { - const lotto = new Lotto([1, 2, 3, 4, 5, 6]); - expect(lotto.format()).toBe("[1, 2, 3, 4, 5, 6]"); - }); - }); - describe("compare 메서드 테스트", () => { test("두 로또 인스턴스 사이의 공통 원소 개수를 반환한다.", () => { const lotto1 = new Lotto([1, 3, 5, 7, 9, 11]); diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index 143aa411c..8a3138369 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -27,7 +27,7 @@ describe("Lottos 클래스", () => { Object.values(RANK).forEach((rank) => { const lottosInstance = new Lottos(PURCHASE_COUNT); lottosInstance.win(rank); - const ranks = lottosInstance._getRanks(); + const ranks = lottosInstance.getRanks(); expect(ranks[rank]).toBe(1); }); }); @@ -42,19 +42,6 @@ describe("Lottos 클래스", () => { }); }); - describe("format 메서드 테스트", () => { - test("모든 Lotto 객체의 format 결과를 줄바꿈으로 연결해 반환한다.", () => { - const lottosInstance = new Lottos(PURCHASE_COUNT); - const formattedLottos = lottosInstance.format(); - const formattedLottoArray = formattedLottos.split("\n"); - - expect(formattedLottoArray).toHaveLength(PURCHASE_COUNT); - formattedLottoArray.forEach((str) => { - expect(str).toMatch(/\[\d+(, \d+){5}\]/); - }); - }); - }); - describe("calculateTotalReturn 메서드 테스트", () => { test("상금을 수익률 형태로 변환해 반환한다.", () => { Object.entries(RANK_TO_PRIZE_MAP).forEach(([rank, prize]) => { diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index a2c6efafd..1a4bcbca7 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,16 +1,19 @@ import { PURCHASE_UNIT } from "../constants/unit.js"; import LottoDrawer from "../models/LottoDrawer.js"; import Lottos from "../models/Lottos.js"; +import Formatter from "../utils/Formatter.js"; import InputView from "../view/InputView.js"; import OutputView from "../view/OutputView.js"; class LottoController { #inputView; #outputView; + #formatter; constructor() { this.#inputView = new InputView(); this.#outputView = new OutputView(); + this.#formatter = new Formatter(); } async run() { @@ -21,7 +24,7 @@ class LottoController { const purchaseCount = purchaseAmount / PURCHASE_UNIT; const lottos = new Lottos(purchaseCount); - this.#outputView.print(`${purchaseAmount}개를 구매했습니다.`); + this.#printPurchaseCount(purchaseCount); this.#printLottos(lottos); const winningNumberString = await this.#inputView.readLineAsync( @@ -42,17 +45,27 @@ class LottoController { } } + #printPurchaseCount(purchaseCount) { + const formattedPurchaseCount = + this.#formatter.formatPurchaseCount(purchaseCount); + this.#outputView.print(formattedPurchaseCount); + } + #printLottos(lottos) { - const formattedLottos = lottos.format(); + const lottoArray = lottos.getLottos(); + const formattedLottos = this.#formatter.formatLottos(lottoArray); this.#outputView.print(formattedLottos); } #printResult(lottos) { - const formattedRanks = lottos.formatResult(); + const ranks = lottos.getRanks(); const totalReturn = lottos.calculateTotalReturn(); - const formattedTotalReturn = `총 수익률은 ${totalReturn}%입니다.`; + + const formattedRankResult = this.#formatter.formatRankResult(ranks); + const formattedTotalReturn = this.#formatter.formatTotalReturn(totalReturn); + this.#outputView.print("당첨 통계"); - this.#outputView.print(formattedRanks); + this.#outputView.print(formattedRankResult); this.#outputView.print(formattedTotalReturn); } } diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index 1e92a3f03..f2f306aba 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -35,10 +35,6 @@ class Lotto { } } - format() { - return `[${this.#numbers.join(", ")}]`; - } - compare(anotherLotto) { return this.#numbers.reduce( (total, num) => (anotherLotto.includes(num) ? total + 1 : total), @@ -49,6 +45,10 @@ class Lotto { includes(num) { return this.#numbers.includes(num); } + + getNumbers() { + return this.#numbers; + } } export default Lotto; diff --git a/src/models/Lottos.js b/src/models/Lottos.js index daf63235f..09f3dff83 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -50,27 +50,11 @@ class Lottos { return totalReturn; } - formatResult() { - return Object.entries(RANK_TO_PRIZE_MAP) - .map( - ([rank, prize]) => - `${RANK_TO_MATCH_STRING_MAP[rank]} (${prize.toLocaleString()}원) - ${ - this.#ranks[rank] - }개` - ) - .join("\n"); - } - - format() { - const formattedLottos = this.#lottos.map((lotto) => lotto.format()); - return formattedLottos.join("\n"); - } - getLottos() { return this.#lottos; } - _getRanks() { + getRanks() { return this.#ranks; } diff --git a/src/utils/Formatter.js b/src/utils/Formatter.js new file mode 100644 index 000000000..f07e47d55 --- /dev/null +++ b/src/utils/Formatter.js @@ -0,0 +1,38 @@ +import { + RANK_TO_MATCH_STRING_MAP, + RANK_TO_PRIZE_MAP, +} from "../constants/rankToPrizeMap.js"; + +class Formatter { + formatLottoNumbers(numbers) { + return `[${numbers.join(", ")}]`; + } + + formatLottos(lottoArray) { + const numbersArray = lottoArray.map((lotto) => lotto.getNumbers()); + const formattedLottos = numbersArray.map((numbers) => + this.formatLottoNumbers(numbers) + ); + return formattedLottos.join("\n"); + } + + formatPurchaseCount(purchaseCount) { + return `${purchaseCount}개를 구매했습니다.`; + } + + formatRankResult(ranks) { + const formattedRankResult = Object.entries(RANK_TO_PRIZE_MAP).map( + ([rank, prize]) => + `${RANK_TO_MATCH_STRING_MAP[rank]} (${prize.toLocaleString()}원) - ${ + ranks[rank] + }개` + ); + return formattedRankResult.join("\n"); + } + + formatTotalReturn(totalReturn) { + return `총 수익률은 ${totalReturn}%입니다.`; + } +} + +export default Formatter; From 1a6610ae3d14aa7ddbd9b5215bd25492aa093264 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 20:30:15 +0900 Subject: [PATCH 34/46] =?UTF-8?q?refactor(constant):=20=EC=9D=B8=ED=92=8B?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/inputMessages.js | 5 +++++ src/controller/LottoController.js | 13 +++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 src/constants/inputMessages.js diff --git a/src/constants/inputMessages.js b/src/constants/inputMessages.js new file mode 100644 index 000000000..76acccc9b --- /dev/null +++ b/src/constants/inputMessages.js @@ -0,0 +1,5 @@ +export const INPUT_MESSAGES = { + PURCHASE_AMOUNT: "구입금액을 입력해 주세요.", + WINNING_NUMBER: "당첨 번호를 입력해 주세요.", + BONUS_NUMBER: "보너스 번호를 입력해 주세요.", +}; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 1a4bcbca7..fe8a05f98 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,3 +1,4 @@ +import { INPUT_MESSAGES } from "../constants/inputMessages.js"; import { PURCHASE_UNIT } from "../constants/unit.js"; import LottoDrawer from "../models/LottoDrawer.js"; import Lottos from "../models/Lottos.js"; @@ -18,7 +19,7 @@ class LottoController { async run() { const purchaseAmount = await this.#inputView.readLineAsync( - "구입금액을 입력해 주세요." + INPUT_MESSAGES.PURCHASE_AMOUNT ); this.#validatePurchaseAmount(purchaseAmount); @@ -27,14 +28,14 @@ class LottoController { this.#printPurchaseCount(purchaseCount); this.#printLottos(lottos); - const winningNumberString = await this.#inputView.readLineAsync( - "당첨 번호를 입력해 주세요." + const winningNumber = await this.#inputView.readLineAsync( + INPUT_MESSAGES.WINNING_NUMBER ); - const bonusNumberString = await this.#inputView.readLineAsync( - "보너스 번호를 입력해 주세요." + const bonusNumber = await this.#inputView.readLineAsync( + INPUT_MESSAGES.BONUS_NUMBER ); - const lottoDrawer = new LottoDrawer(winningNumberString, bonusNumberString); + const lottoDrawer = new LottoDrawer(winningNumber, bonusNumber); lottoDrawer.run(lottos); this.#printResult(lottos); } From 3246ebc6db3b0a278d1d9bb97232140a76a779de Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 20:34:13 +0900 Subject: [PATCH 35/46] =?UTF-8?q?refactor(LottoController):=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=B6=9C=EB=A0=A5=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=84=B8=EB=B6=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/ApplicationTest.js | 194 +++++++++++++++--------------- src/controller/LottoController.js | 13 +- 2 files changed, 107 insertions(+), 100 deletions(-) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 800e7ae3d..872380c9c 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,97 +1,97 @@ -// import App from "../src/App.js"; -// import { MissionUtils } from "@woowacourse/mission-utils"; - -// const mockQuestions = (inputs) => { -// MissionUtils.Console.readLineAsync = jest.fn(); - -// MissionUtils.Console.readLineAsync.mockImplementation(() => { -// const input = inputs.shift(); - -// return Promise.resolve(input); -// }); -// }; - -// const mockRandoms = (numbers) => { -// MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); -// numbers.reduce((acc, number) => { -// return acc.mockReturnValueOnce(number); -// }, MissionUtils.Random.pickUniqueNumbersInRange); -// }; - -// const getLogSpy = () => { -// const logSpy = jest.spyOn(MissionUtils.Console, "print"); -// logSpy.mockClear(); -// return logSpy; -// }; - -// const runException = async (input) => { -// // given -// 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"]; - -// mockRandoms([RANDOM_NUMBERS_TO_END]); -// mockQuestions([input, ...INPUT_NUMBERS_TO_END]); - -// // when -// const app = new App(); -// await app.run(); - -// // then -// expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); -// }; - -// describe("로또 테스트", () => { -// beforeEach(() => { -// jest.restoreAllMocks(); -// }); - -// test("기능 테스트", async () => { -// // given -// const logSpy = getLogSpy(); - -// mockRandoms([ -// [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], -// ]); -// mockQuestions(["8000", "1,2,3,4,5,6", "7"]); - -// // when -// const app = new App(); -// await app.run(); - -// // 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%입니다.", -// ]; - -// logs.forEach((log) => { -// expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); -// }); -// }); - -// test("예외 테스트", async () => { -// await runException("1000j"); -// }); -// }); +import App from "../src/App.js"; +import { MissionUtils } from "@woowacourse/mission-utils"; + +const mockQuestions = (inputs) => { + MissionUtils.Console.readLineAsync = jest.fn(); + + MissionUtils.Console.readLineAsync.mockImplementation(() => { + const input = inputs.shift(); + + return Promise.resolve(input); + }); +}; + +const mockRandoms = (numbers) => { + MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickUniqueNumbersInRange); +}; + +const getLogSpy = () => { + const logSpy = jest.spyOn(MissionUtils.Console, "print"); + logSpy.mockClear(); + return logSpy; +}; + +const runException = async (input) => { + // given + 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"]; + + mockRandoms([RANDOM_NUMBERS_TO_END]); + mockQuestions([input, ...INPUT_NUMBERS_TO_END]); + + // when + const app = new App(); + await app.run(); + + // then + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); +}; + +describe("로또 테스트", () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + test("기능 테스트", async () => { + // given + const logSpy = getLogSpy(); + + mockRandoms([ + [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], + ]); + mockQuestions(["8000", "1,2,3,4,5,6", "7"]); + + // when + const app = new App(); + await app.run(); + + // 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%입니다.", + ]; + + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); + + test("예외 테스트", async () => { + await runException("1000j"); + }); +}); diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index fe8a05f98..db6a6e953 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -62,11 +62,18 @@ class LottoController { const ranks = lottos.getRanks(); const totalReturn = lottos.calculateTotalReturn(); - const formattedRankResult = this.#formatter.formatRankResult(ranks); - const formattedTotalReturn = this.#formatter.formatTotalReturn(totalReturn); - this.#outputView.print("당첨 통계"); + this.#printRankResult(ranks); + this.#printTotalReturn(totalReturn); + } + + #printRankResult(ranks) { + const formattedRankResult = this.#formatter.formatRankResult(ranks); this.#outputView.print(formattedRankResult); + } + + #printTotalReturn(totalReturn) { + const formattedTotalReturn = this.#formatter.formatTotalReturn(totalReturn); this.#outputView.print(formattedTotalReturn); } } From 87319e23f76e59b4771f5d5da66ed543ba463784 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 20:40:01 +0900 Subject: [PATCH 36/46] =?UTF-8?q?refactor(LottoController):=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EA=B5=AC=EB=AC=B8=20=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/LottoController.js | 35 ++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index db6a6e953..5fead814c 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -18,16 +18,34 @@ class LottoController { } async run() { + const purchaseCount = await this.#readPurchaseCount(); + const lottos = new Lottos(purchaseCount); + this.#printInputResult(purchaseCount, lottos); + + const { winningNumber, bonusNumber } = await this.#readDrawnNumbers(); + const lottoDrawer = new LottoDrawer(winningNumber, bonusNumber); + lottoDrawer.run(lottos); + + this.#printResult(lottos); + } + + async #readPurchaseCount() { const purchaseAmount = await this.#inputView.readLineAsync( INPUT_MESSAGES.PURCHASE_AMOUNT ); this.#validatePurchaseAmount(purchaseAmount); const purchaseCount = purchaseAmount / PURCHASE_UNIT; - const lottos = new Lottos(purchaseCount); - this.#printPurchaseCount(purchaseCount); - this.#printLottos(lottos); + return purchaseCount; + } + #validatePurchaseAmount(purchaseAmount) { + if (purchaseAmount % PURCHASE_UNIT !== 0) { + throw new Error("[ERROR] 구입 금액은 1000원 단위로 입력해야 합니다."); + } + } + + async #readDrawnNumbers() { const winningNumber = await this.#inputView.readLineAsync( INPUT_MESSAGES.WINNING_NUMBER ); @@ -35,15 +53,12 @@ class LottoController { INPUT_MESSAGES.BONUS_NUMBER ); - const lottoDrawer = new LottoDrawer(winningNumber, bonusNumber); - lottoDrawer.run(lottos); - this.#printResult(lottos); + return { winningNumber, bonusNumber }; } - #validatePurchaseAmount(purchaseAmount) { - if (purchaseAmount % PURCHASE_UNIT !== 0) { - throw new Error("[ERROR] 구입 금액은 1000원 단위로 입력해야 합니다."); - } + #printInputResult(purchaseCount, lottos) { + this.#printPurchaseCount(purchaseCount); + this.#printLottos(lottos); } #printPurchaseCount(purchaseCount) { From 389c2ea27e719d925e18f0838a2d0ae345584f33 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 20:41:24 +0900 Subject: [PATCH 37/46] =?UTF-8?q?refactor(constant):=20rank=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=83=81=EC=88=98=20=ED=8C=8C=EC=9D=BC=EB=AA=85=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 --- __tests__/DrawnNumbersTest.js | 2 +- __tests__/LottosTest.js | 2 +- src/constants/{rankToPrizeMap.js => rank.js} | 1 - src/models/Lottos.js | 6 +----- src/utils/Formatter.js | 2 +- 5 files changed, 4 insertions(+), 9 deletions(-) rename src/constants/{rankToPrizeMap.js => rank.js} (97%) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 362445466..677182305 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -1,4 +1,4 @@ -import { RANK } from "../src/constants/rankToPrizeMap"; +import { RANK } from "../src/constants/rank.js"; import Lotto from "../src/domains/Lotto"; import DrawnNumbers from "../src/models/DrawnNumbers"; diff --git a/__tests__/LottosTest.js b/__tests__/LottosTest.js index 8a3138369..a8814701f 100644 --- a/__tests__/LottosTest.js +++ b/__tests__/LottosTest.js @@ -1,4 +1,4 @@ -import { RANK, RANK_TO_PRIZE_MAP } from "../src/constants/rankToPrizeMap.js"; +import { RANK, RANK_TO_PRIZE_MAP } from "../src/constants/rank.js"; import { PURCHASE_UNIT } from "../src/constants/unit.js"; import Lotto from "../src/domains/Lotto.js"; import Lottos from "../src/models/Lottos.js"; diff --git a/src/constants/rankToPrizeMap.js b/src/constants/rank.js similarity index 97% rename from src/constants/rankToPrizeMap.js rename to src/constants/rank.js index cfb705e48..3bfb1a670 100644 --- a/src/constants/rankToPrizeMap.js +++ b/src/constants/rank.js @@ -4,7 +4,6 @@ export const RANK = { "3RD": "3rd", "4TH": "4th", "5TH": "5th", - ETC: "etc", }; export const RANK_TO_PRIZE_MAP = { diff --git a/src/models/Lottos.js b/src/models/Lottos.js index 09f3dff83..c940abadb 100644 --- a/src/models/Lottos.js +++ b/src/models/Lottos.js @@ -1,9 +1,5 @@ import { Random } from "@woowacourse/mission-utils"; -import { - RANK, - RANK_TO_MATCH_STRING_MAP, - RANK_TO_PRIZE_MAP, -} from "../constants/rankToPrizeMap.js"; +import { RANK, RANK_TO_PRIZE_MAP } from "../constants/rank.js"; import { PURCHASE_UNIT } from "../constants/unit.js"; import { LOTTO_MAX_VALUE, diff --git a/src/utils/Formatter.js b/src/utils/Formatter.js index f07e47d55..fd8c95edc 100644 --- a/src/utils/Formatter.js +++ b/src/utils/Formatter.js @@ -1,7 +1,7 @@ import { RANK_TO_MATCH_STRING_MAP, RANK_TO_PRIZE_MAP, -} from "../constants/rankToPrizeMap.js"; +} from "../constants/rank.js"; class Formatter { formatLottoNumbers(numbers) { From 3ee0d81d1733b11f50ea8a9be8147eba059ab50b Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 20:47:25 +0900 Subject: [PATCH 38/46] =?UTF-8?q?feat(utils):=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20prefix=20=EC=B2=98=EB=A6=AC=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/error.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/utils/error.js diff --git a/src/utils/error.js b/src/utils/error.js new file mode 100644 index 000000000..e09f6c3fd --- /dev/null +++ b/src/utils/error.js @@ -0,0 +1 @@ +export const error = (message) => `[ERROR] ${message}`; From 6291c1df4fc6268e1f5617f05dab2cd7cbf923fd Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 21:13:49 +0900 Subject: [PATCH 39/46] =?UTF-8?q?refactor(constant):=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 20 +++++++++------ __tests__/LottoTest.js | 9 ++++--- src/constants/errorMessages.js | 42 +++++++++++++++++++++++++++++++ src/controller/LottoController.js | 7 +++++- src/domains/Lotto.js | 13 ++++------ src/models/DrawnNumbers.js | 20 +++++++-------- 6 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 src/constants/errorMessages.js diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 677182305..88bb77b02 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -1,3 +1,7 @@ +import { + BONUS_NUMBER_ERROR_MESSAGES, + WINNING_NUMBER_ERROR_MESSAGES, +} from "../src/constants/errorMessages.js"; import { RANK } from "../src/constants/rank.js"; import Lotto from "../src/domains/Lotto"; import DrawnNumbers from "../src/models/DrawnNumbers"; @@ -7,49 +11,49 @@ describe("DrawnNumbers 클래스", () => { test("당첨 번호 입력이 없으면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers("", "1"); - }).toThrow("[ERROR]"); + }).toThrow(WINNING_NUMBER_ERROR_MESSAGES.NONEMPTY); }); test("당첨 번호 입력이 공백이면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers(" ", "1"); - }).toThrow("[ERROR]"); + }).toThrow(WINNING_NUMBER_ERROR_MESSAGES.NONEMPTY); }); test("당첨 번호가 쉼표 외 구분자로 분리되어 있으면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers("1. 2. 3. 4. 5. 6", "1"); - }).toThrow("[ERROR]"); + }).toThrow(WINNING_NUMBER_ERROR_MESSAGES.DELIMITER); }); test("보너스 번호 입력이 없으면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers("1, 2, 3, 4, 5, 6", ""); - }).toThrow("[ERROR]"); + }).toThrow(BONUS_NUMBER_ERROR_MESSAGES.NONEMPTY); }); test("보너스 번호 입력이 공백이면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers("1, 2, 3, 4, 5, 6", " "); - }).toThrow("[ERROR]"); + }).toThrow(BONUS_NUMBER_ERROR_MESSAGES.NONEMPTY); }); test("보너스 번호가 1보다 작으면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers("1, 2, 3, 4, 5, 6", "0"); - }).toThrow("[ERROR]"); + }).toThrow(BONUS_NUMBER_ERROR_MESSAGES.MIN_VALUE); }); test("보너스 번호가 45보다 크면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers("1, 2, 3, 4, 5, 6", "46"); - }).toThrow("[ERROR]"); + }).toThrow(BONUS_NUMBER_ERROR_MESSAGES.MAX_VALUE); }); test("보너스 번호가 이미 당첨 번호에 포함되어 있으면 예외가 발생한다.", () => { expect(() => { new DrawnNumbers("1, 2, 3, 4, 5, 6", "1"); - }).toThrow("[ERROR]"); + }).toThrow(BONUS_NUMBER_ERROR_MESSAGES.UNIQUE); }); }); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 0097230ce..8de2217cc 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,3 +1,4 @@ +import { LOTTO_ERROR_MESSAGES } from "../src/constants/errorMessages.js"; import Lotto from "../src/domains/Lotto.js"; describe("Lotto 클래스", () => { @@ -5,25 +6,25 @@ describe("Lotto 클래스", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); + }).toThrow(LOTTO_ERROR_MESSAGES.LENGTH); }); test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); + }).toThrow(LOTTO_ERROR_MESSAGES.UNIQUE); }); test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([0, 2, 3, 4, 5, 6]); - }).toThrow("[ERROR]"); + }).toThrow(LOTTO_ERROR_MESSAGES.MIN_VALUE); }); test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 46]); - }).toThrow("[ERROR]"); + }).toThrow(LOTTO_ERROR_MESSAGES.MAX_VALUE); }); }); diff --git a/src/constants/errorMessages.js b/src/constants/errorMessages.js new file mode 100644 index 000000000..43c10f661 --- /dev/null +++ b/src/constants/errorMessages.js @@ -0,0 +1,42 @@ +import { error } from "../utils/error.js"; +import { LOTTO_MAX_VALUE, LOTTO_SIZE } from "./lotto.js"; +import { PURCHASE_UNIT } from "./unit.js"; + +export const PURCHASE_ERROR_MESSAGES = { + NONEMPTY: error("구입 금액을 입력해 주세요."), + UNIT: error(`구입 금액은 ${PURCHASE_UNIT}원 단위로 입력해야 합니다.`), +}; + +export const LOTTO_ERROR_MESSAGES = { + UNIQUE: error("로또 번호는 중복될 수 없습니다."), + LENGTH: error(`로또 번호는 ${LOTTO_SIZE}개여야 합니다.`), + MIN_VALUE: error( + `로또 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ), + MAX_VALUE: error( + `로또 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ), +}; + +export const WINNING_NUMBER_ERROR_MESSAGES = { + NONEMPTY: error("당첨 번호를 입력해 주세요."), + DELIMITER: error("당첨 번호는 쉼표로 구분되어야 합니다."), +}; + +export const BONUS_NUMBER_ERROR_MESSAGES = { + NONEMPTY: error("보너스 번호를 입력해 주세요."), + MIN_VALUE: error( + `보너스 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ), + MAX_VALUE: error( + `보너스 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` + ), + UNIQUE: error("이미 당첨 번호에 포함된 번호입니다."), +}; + +export const ERROR_MESSAGES = { + PURCHASE: PURCHASE_ERROR_MESSAGES, + LOTTO: LOTTO_ERROR_MESSAGES, + WINNING_NUMBER: WINNING_NUMBER_ERROR_MESSAGES, + BONUS_NUMBER: BONUS_NUMBER_ERROR_MESSAGES, +}; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 5fead814c..cff863a59 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,3 +1,4 @@ +import { PURCHASE_ERROR_MESSAGES } from "../constants/errorMessages.js"; import { INPUT_MESSAGES } from "../constants/inputMessages.js"; import { PURCHASE_UNIT } from "../constants/unit.js"; import LottoDrawer from "../models/LottoDrawer.js"; @@ -40,8 +41,12 @@ class LottoController { } #validatePurchaseAmount(purchaseAmount) { + if (purchaseAmount === "") { + throw new Error(PURCHASE_ERROR_MESSAGES.NONEMPTY); + } + if (purchaseAmount % PURCHASE_UNIT !== 0) { - throw new Error("[ERROR] 구입 금액은 1000원 단위로 입력해야 합니다."); + throw new Error(PURCHASE_ERROR_MESSAGES.UNIT); } } diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index f2f306aba..a9aeaadb8 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -1,3 +1,4 @@ +import { LOTTO_ERROR_MESSAGES } from "../constants/errorMessages.js"; import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE, @@ -14,24 +15,20 @@ class Lotto { #validate(numbers) { if (numbers.length !== LOTTO_SIZE) { - throw new Error(`[ERROR] 로또 번호는 ${LOTTO_SIZE}개여야 합니다.`); + throw new Error(LOTTO_ERROR_MESSAGES.LENGTH); } const numberSet = new Set(numbers); if (numbers.length > numberSet.size) { - throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + throw new Error(LOTTO_ERROR_MESSAGES.UNIQUE); } if (numbers.some((num) => num < LOTTO_MIN_VALUE)) { - throw new Error( - `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` - ); + throw new Error(LOTTO_ERROR_MESSAGES.MIN_VALUE); } if (numbers.some((num) => num > LOTTO_MAX_VALUE)) { - throw new Error( - `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` - ); + throw new Error(LOTTO_ERROR_MESSAGES.MAX_VALUE); } } diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index bd8509ddf..cc70049e3 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -1,3 +1,7 @@ +import { + BONUS_NUMBER_ERROR_MESSAGES, + WINNING_NUMBER_ERROR_MESSAGES, +} from "../constants/errorMessages.js"; import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE } from "../constants/lotto.js"; import Lotto from "../domains/Lotto.js"; @@ -32,37 +36,33 @@ class DrawnNumbers { #validateWinningNumbers(numbersString) { const trimmedString = numbersString.trim(); if (trimmedString === "") { - throw new Error("[ERROR] 당첨 번호를 입력해 주세요."); + throw new Error(WINNING_NUMBER_ERROR_MESSAGES.NONEMPTY); } const format = /^\s*\d+(\s*,\s*\d+)*\s*$/; if (!format.test(trimmedString)) { - throw new Error("[ERROR] 당첨 번호는 쉼표로 구분되어야 합니다."); + throw new Error(WINNING_NUMBER_ERROR_MESSAGES.DELIMITER); } } #validateBonusNumber(bonusNumberString) { const trimmedString = bonusNumberString.trim(); if (trimmedString === "") { - throw new Error("[ERROR] 보너스 번호를 입력해 주세요."); + throw new Error(BONUS_NUMBER_ERROR_MESSAGES.NONEMPTY); } const bonusNumber = Number(trimmedString); if (bonusNumber < LOTTO_MIN_VALUE) { - throw new Error( - `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` - ); + throw new Error(BONUS_NUMBER_ERROR_MESSAGES.MIN_VALUE); } if (bonusNumber > LOTTO_MAX_VALUE) { - throw new Error( - `[ERROR] 로또 번호는 ${LOTTO_MIN_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.` - ); + throw new Error(BONUS_NUMBER_ERROR_MESSAGES.MAX_VALUE); } if (this.#winningNumbers.includes(bonusNumber)) { - throw new Error("[ERROR] 이미 당첨 번호에 포함된 번호입니다."); + throw new Error(BONUS_NUMBER_ERROR_MESSAGES.UNIQUE); } } } From d149d7bf5c2d830a7756bfeaa27fbb42efef4ef4 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 21:22:31 +0900 Subject: [PATCH 40/46] =?UTF-8?q?refactor(RANK):=20key=EA=B0=92=EC=9D=B4?= =?UTF-8?q?=20=EC=98=81=EC=96=B4=EB=A1=9C=20=EC=8B=9C=EC=9E=91=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 10 +++++----- src/constants/rank.js | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 88bb77b02..9642bbcd3 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -62,7 +62,7 @@ describe("DrawnNumbers 클래스", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 5, 6]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( - RANK["1ST"] + RANK.FIRST ); }); @@ -70,7 +70,7 @@ describe("DrawnNumbers 클래스", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 5, 7]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( - RANK["2ND"] + RANK.SECOND ); }); @@ -78,7 +78,7 @@ describe("DrawnNumbers 클래스", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 5, 8]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( - RANK["3RD"] + RANK.THIRD ); }); @@ -86,7 +86,7 @@ describe("DrawnNumbers 클래스", () => { const lottoInstance = new Lotto([1, 2, 3, 4, 7, 8]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( - RANK["4TH"] + RANK.FOURTH ); }); @@ -94,7 +94,7 @@ describe("DrawnNumbers 클래스", () => { const lottoInstance = new Lotto([1, 2, 3, 7, 8, 9]); const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7"); expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe( - RANK["5TH"] + RANK.FIFTH ); }); }); diff --git a/src/constants/rank.js b/src/constants/rank.js index 3bfb1a670..7239f6164 100644 --- a/src/constants/rank.js +++ b/src/constants/rank.js @@ -1,23 +1,23 @@ export const RANK = { - "1ST": "1st", - "2ND": "2nd", - "3RD": "3rd", - "4TH": "4th", - "5TH": "5th", + FIRST: "1st", + SECOND: "2nd", + THIRD: "3rd", + FOURTH: "4th", + FIFTH: "5th", }; export const RANK_TO_PRIZE_MAP = { - [RANK["1ST"]]: 2000000000, - [RANK["2ND"]]: 30000000, - [RANK["3RD"]]: 1500000, - [RANK["4TH"]]: 50000, - [RANK["5TH"]]: 5000, + [RANK.FIRST]: 2000000000, + [RANK.SECOND]: 30000000, + [RANK.THIRD]: 1500000, + [RANK.FOURTH]: 50000, + [RANK.FIFTH]: 5000, }; export const RANK_TO_MATCH_STRING_MAP = { - [RANK["1ST"]]: "6개 일치", - [RANK["2ND"]]: "5개 일치, 보너스 볼 일치", - [RANK["3RD"]]: "5개 일치", - [RANK["4TH"]]: "4개 일치", - [RANK["5TH"]]: "3개 일치", + [RANK.FIRST]: "6개 일치", + [RANK.SECOND]: "5개 일치, 보너스 볼 일치", + [RANK.THIRD]: "5개 일치", + [RANK.FOURTH]: "4개 일치", + [RANK.FIFTH]: "3개 일치", }; From 16984f84b6f2e3912b01419ce5428f50a21329a5 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 21:36:47 +0900 Subject: [PATCH 41/46] =?UTF-8?q?refactor(DrawnNumbers):=20rank=20?= =?UTF-8?q?=EC=83=81=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/models/DrawnNumbers.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/models/DrawnNumbers.js b/src/models/DrawnNumbers.js index cc70049e3..fb889f22e 100644 --- a/src/models/DrawnNumbers.js +++ b/src/models/DrawnNumbers.js @@ -3,6 +3,7 @@ import { WINNING_NUMBER_ERROR_MESSAGES, } from "../constants/errorMessages.js"; import { LOTTO_MAX_VALUE, LOTTO_MIN_VALUE } from "../constants/lotto.js"; +import { RANK } from "../constants/rank.js"; import Lotto from "../domains/Lotto.js"; class DrawnNumbers { @@ -19,11 +20,11 @@ class DrawnNumbers { const matchCount = lotto.compare(this.#winningNumbers); const hasBonus = lotto.includes(this.#bonusNumber); - if (matchCount === 6) return "1st"; - if (matchCount === 5 && hasBonus) return "2nd"; - if (matchCount === 5) return "3rd"; - if (matchCount === 4) return "4th"; - if (matchCount === 3) return "5th"; + if (matchCount === 6) return RANK.FIRST; + if (matchCount === 5 && hasBonus) return RANK.SECOND; + if (matchCount === 5) return RANK.THIRD; + if (matchCount === 4) return RANK.FOURTH; + if (matchCount === 3) return RANK.FIFTH; return undefined; } From 744131a2ac4832d54d495cde9a59a3e840c53e79 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 21:45:38 +0900 Subject: [PATCH 42/46] =?UTF-8?q?docs(README):=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 4c458672c..22e336b9d 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,29 @@ 6. 랜덤 생성된 로또 번호들마다 당첨 여부 확인 7. 총 수익률 계산 8. 당첨 통계(1-5등 개수, 총 수익률) 출력 + +## 폴더 구조 + +```bash +src +├─ constants +│ ├─ errorMessages.js +│ ├─ inputMessages.js +│ ├─ lotto.js +│ ├─ rank.js +│ └─ unit.js +├─ controller +│ └─ LottoController.js // 전체 로또 추첨 흐름 제어 +├─ domains +│ └─ Lotto.js // 단일 로또 상태 관리 +├─ models +│ ├─ DrawnNumbers.js // 당첨 번호 + 보너스 번호 상태 관리 +│ ├─ LottoDrawer.js // DrawnNumbers -> Lottos 상태 업데이트 +│ └─ Lottos.js // 사용자 로또 번호 상태 관리 +├─ utils +│ ├─ error.js +│ └─ Formatter.js // 출력문 형식 관리 +└─ view + ├─ InputView.js // 사용자 입력 처리 + └─ OutputView.js // 출력 처리 +``` From 2ae2b688150e9258efcdec8d8c190eb793da4d57 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 21:48:57 +0900 Subject: [PATCH 43/46] =?UTF-8?q?feat(LottoTest):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EA=B0=9C=EC=88=98=EA=B0=80=206=EA=B0=9C?= =?UTF-8?q?=20=EB=AF=B8=EB=A7=8C=EC=9D=B8=20=EA=B2=BD=EC=9A=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__/LottoTest.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 8de2217cc..70b5e500b 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -3,7 +3,13 @@ import Lotto from "../src/domains/Lotto.js"; describe("Lotto 클래스", () => { describe("생성자 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { + test("로또 번호의 개수가 6개 미만이면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5]); + }).toThrow(LOTTO_ERROR_MESSAGES.LENGTH); + }); + + test("로또 번호의 개수가 6개를 넘어가면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 6, 7]); }).toThrow(LOTTO_ERROR_MESSAGES.LENGTH); From 85af63d6ebd5c58271175178b053395ab67e145e Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 23:47:33 +0900 Subject: [PATCH 44/46] =?UTF-8?q?style:=20js=20=ED=99=95=EC=9E=A5=EC=9E=90?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/DrawnNumbersTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/DrawnNumbersTest.js b/__tests__/DrawnNumbersTest.js index 9642bbcd3..aa69cd9bc 100644 --- a/__tests__/DrawnNumbersTest.js +++ b/__tests__/DrawnNumbersTest.js @@ -3,8 +3,8 @@ import { WINNING_NUMBER_ERROR_MESSAGES, } from "../src/constants/errorMessages.js"; import { RANK } from "../src/constants/rank.js"; -import Lotto from "../src/domains/Lotto"; -import DrawnNumbers from "../src/models/DrawnNumbers"; +import Lotto from "../src/domains/Lotto.js"; +import DrawnNumbers from "../src/models/DrawnNumbers.js"; describe("DrawnNumbers 클래스", () => { describe("생성자 테스트", () => { From a9213feec817cd48898c9cdac3ea20df5d59baf5 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 23:55:08 +0900 Subject: [PATCH 45/46] =?UTF-8?q?fix:=20if=EB=AC=B8=20=EC=A4=91=EA=B4=84?= =?UTF-8?q?=ED=98=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/Lotto.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/domains/Lotto.js b/src/domains/Lotto.js index a9aeaadb8..ec474c475 100644 --- a/src/domains/Lotto.js +++ b/src/domains/Lotto.js @@ -14,22 +14,18 @@ class Lotto { } #validate(numbers) { - if (numbers.length !== LOTTO_SIZE) { + if (numbers.length !== LOTTO_SIZE) throw new Error(LOTTO_ERROR_MESSAGES.LENGTH); - } const numberSet = new Set(numbers); - if (numbers.length > numberSet.size) { + if (numbers.length > numberSet.size) throw new Error(LOTTO_ERROR_MESSAGES.UNIQUE); - } - if (numbers.some((num) => num < LOTTO_MIN_VALUE)) { + if (numbers.some((num) => num < LOTTO_MIN_VALUE)) throw new Error(LOTTO_ERROR_MESSAGES.MIN_VALUE); - } - if (numbers.some((num) => num > LOTTO_MAX_VALUE)) { + if (numbers.some((num) => num > LOTTO_MAX_VALUE)) throw new Error(LOTTO_ERROR_MESSAGES.MAX_VALUE); - } } compare(anotherLotto) { From 41d069fa8a6ff09985efe2f954dca98a591fb1c2 Mon Sep 17 00:00:00 2001 From: gustn99 Date: Mon, 3 Nov 2025 23:59:21 +0900 Subject: [PATCH 46/46] =?UTF-8?q?fix:=20=EC=B6=9C=EB=A0=A5=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/LottoController.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index cff863a59..1e65ce2b4 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -82,7 +82,9 @@ class LottoController { const ranks = lottos.getRanks(); const totalReturn = lottos.calculateTotalReturn(); + this.#outputView.print(); this.#outputView.print("당첨 통계"); + this.#outputView.print("---"); this.#printRankResult(ranks); this.#printTotalReturn(totalReturn); }