From ac70aedb36aa2eb7ffa01d31c9fd240f737bd4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Sun, 2 Nov 2025 22:40:02 +0900 Subject: [PATCH 01/29] =?UTF-8?q?docs(readme):=20=EA=B8=B0=EB=8A=A5?= =?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 구현하는 로또 미션의 흐름에 따라 기능들을 나누고 이를 effect가 있는 액션인지 순수함수인 계산인지를 구분하였습니다. --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 15bb106b5..3d107aecc 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ # javascript-lotto-precourse +# 로또 시뮬레이터 + +# 기능 목록 +- [ ] `사용자의 입력을 다루는 부수효과 액션` + - [ ] 로또 구입 금액을 읽는다. + - [ ] 로또 당첨 번호를 읽는다. + - [ ] 보너스 번호를 읽는다. +- [ ] `입력값을 필요한 형태로 파싱 계산` + - [ ] 구입 금액 문자열을 숫자 타입으로 변환한다. (재사용성 검토) + - [ ] 구입 금액을 로또 1장의 가격으로 나눈다. + - [ ] 로또 당첨 번호를 쉼표(,)를 기준으로 나누어 숫자 배열을 생성한다. + - [ ] 보너스 번호 문자열을 숫자 타입으로 변환한다. +- [ ] `사용자의 입력값 검증 계산` + - [ ] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. + - [ ] 로또 당첨 번호가 6개인지 검증한다. + - [ ] 로또 당첨 번호가 중복되지 않는 지 검증한다. + - [ ] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. + - [ ] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. + - [ ] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. +- [ ] `검증 결과에 따라 에러 발생 부수효과 액션` + - [ ] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. +- [ ] `사용자에게 재입력을 받는 부수효과 액션` + - [ ] 로또 구입 금액에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [ ] 당첨 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [ ] 보너스 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. +- [ ] `난수를 생성하는 부수효과 액션` + - [ ] 1 <= n <= 45 범위의 난수 n을 생성한다. +- [ ] `구매량 만큼 로또를 만드는 계산` + - [ ] <당첨 번호 6개> 의 데이터를 생성한다. + - [ ] 구매량 만큼의 로또 데이터를 생성한다. +- [ ] `당첨 번호와 로또 번호를 비교/채점하는 계산` + - [ ] 당첨 번호와 일치하는 번호의 수를 계산한다. + - [ ] 보너스 번호가 당첨 번호와 일치하는지 계산한다. + - [ ] 로또의 등수를 계산한다. +- [ ] `로또 수익률을 계산` + - [ ] 전체 상금을 계산한다. + - [ ] 수익률을 계산한다. +- [ ] `로또 시뮬레이션의 결과를 출력하는 부수효과 액션` + - [ ] 발행한 로또의 수량과 번호을 출력한다. + - [ ] 당첨 내역을 출력한다. + - [ ] 수익률을 출력한다. +- [ ] `각 기능 목록 구현을 진행하며 단위테스트 진행한다.` From 5b738c5455d58509f89636e39d994ff3de5495bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Sun, 2 Nov 2025 23:23:38 +0900 Subject: [PATCH 02/29] =?UTF-8?q?docs(readme):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9D=84=20effect=EC=99=80=20pure=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존의 작성된 기능 목록을 effect와 pure 기준으로 구조적으로 분리했습니다. 분리한 이유는 순수 계산 기능을 먼저 구현하기 위함입니다. --- README.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3d107aecc..70c8304cd 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,7 @@ # 로또 시뮬레이터 # 기능 목록 -- [ ] `사용자의 입력을 다루는 부수효과 액션` - - [ ] 로또 구입 금액을 읽는다. - - [ ] 로또 당첨 번호를 읽는다. - - [ ] 보너스 번호를 읽는다. +### Pure(순수 계산: 계산/검증/도메인) - [ ] `입력값을 필요한 형태로 파싱 계산` - [ ] 구입 금액 문자열을 숫자 타입으로 변환한다. (재사용성 검토) - [ ] 구입 금액을 로또 1장의 가격으로 나눈다. @@ -18,14 +15,6 @@ - [ ] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [ ] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [ ] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. -- [ ] `검증 결과에 따라 에러 발생 부수효과 액션` - - [ ] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. -- [ ] `사용자에게 재입력을 받는 부수효과 액션` - - [ ] 로또 구입 금액에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. - - [ ] 당첨 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. - - [ ] 보너스 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. -- [ ] `난수를 생성하는 부수효과 액션` - - [ ] 1 <= n <= 45 범위의 난수 n을 생성한다. - [ ] `구매량 만큼 로또를 만드는 계산` - [ ] <당첨 번호 6개> 의 데이터를 생성한다. - [ ] 구매량 만큼의 로또 데이터를 생성한다. @@ -36,8 +25,21 @@ - [ ] `로또 수익률을 계산` - [ ] 전체 상금을 계산한다. - [ ] 수익률을 계산한다. -- [ ] `로또 시뮬레이션의 결과를 출력하는 부수효과 액션` + +### Effect(부수효과 액션: 입출력/난수/에러) +- [ ] `사용자의 입력 액션` + - [ ] 로또 구입 금액을 읽는다. + - [ ] 로또 당첨 번호를 읽는다. + - [ ] 보너스 번호를 읽는다. +- [ ] `에러 발생 액션` + - [ ] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. +- [ ] `사용자에게 재입력 액션` + - [ ] 로또 구입 금액에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [ ] 당첨 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [ ] 보너스 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. +- [ ] `난수를 생성 액션` + - [ ] 1 <= n <= 45 범위의 난수 n을 생성한다. +- [ ] `로또 시뮬레이션 결과 출력 액션` - [ ] 발행한 로또의 수량과 번호을 출력한다. - [ ] 당첨 내역을 출력한다. - - [ ] 수익률을 출력한다. -- [ ] `각 기능 목록 구현을 진행하며 단위테스트 진행한다.` + - [ ] 수익률을 출력한다. \ No newline at end of file From d002db0d90802182d9f4713aa5467db394a5726f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 01:56:35 +0900 Subject: [PATCH 03/29] =?UTF-8?q?feat(constants):=20=EB=A1=9C=EB=98=90=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/constants.js diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 000000000..f77f827bc --- /dev/null +++ b/src/constants.js @@ -0,0 +1,14 @@ +export const LOTTO_CONSTANTS = Object.freeze({ + TICKET_PRICE: 1000, + MIN_NUMBER: 1, + MAX_NUMBER: 45, + NUMBERS_PER_TICKET: 6, +}); + +export const PRIZE_TABLE = Object.freeze({ + 1: 2_000_000_000, + 2: 30_000_000, // 5개 일치 + 보너스 번호 일치 + 3: 1_500_000, + 4: 50_000, + 5: 5_000, +}); \ No newline at end of file From 602187bf454362080ad5ad1685447fd03a12ce0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 03:00:16 +0900 Subject: [PATCH 04/29] feat(ranking): calculate matched numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 당첨 번호와 티켓 사이에 일치하는 숫자의 개수를 계산하는 함수를 구현하였습니다. set를 사용해서 중복되지 않았을 때의 길이에서 Set()의 size를 빼주는 방식으로 구현했습니다. --- README.md | 2 +- __tests__/DomainTest.js | 13 +++++++++++++ src/domains/ranking.js | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 __tests__/DomainTest.js create mode 100644 src/domains/ranking.js diff --git a/README.md b/README.md index 70c8304cd..ce62ae960 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ - [ ] <당첨 번호 6개> 의 데이터를 생성한다. - [ ] 구매량 만큼의 로또 데이터를 생성한다. - [ ] `당첨 번호와 로또 번호를 비교/채점하는 계산` - - [ ] 당첨 번호와 일치하는 번호의 수를 계산한다. + - [x] 당첨 번호와 일치하는 번호의 수를 계산한다. - [ ] 보너스 번호가 당첨 번호와 일치하는지 계산한다. - [ ] 로또의 등수를 계산한다. - [ ] `로또 수익률을 계산` diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js new file mode 100644 index 000000000..03236b142 --- /dev/null +++ b/__tests__/DomainTest.js @@ -0,0 +1,13 @@ +import { calculateMatchCount } from "../src/domains/ranking"; +describe("순위 결정 관련 비지니스 로직", () => { + test("일치하는 개수를 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const winning = [1, 2, 3, 4, 5, 6]; + expect(calculateMatchCount(ticket, winning)).toBe(6); + }); + test("하나도 일치하지 않을 경우 0을 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const winning = [7, 8, 9, 10, 11, 12]; + expect(calculateMatchCount(ticket, winning)).toBe(0); + }); +}) \ No newline at end of file diff --git a/src/domains/ranking.js b/src/domains/ranking.js new file mode 100644 index 000000000..cd38837e5 --- /dev/null +++ b/src/domains/ranking.js @@ -0,0 +1,4 @@ +export function calculateMatchCount(ticket, winning, numbersPerTicket = 6) { + const uniqueNumbers = new Set([...ticket, ...winning]); + return numbersPerTicket * 2 - uniqueNumbers.size; +} \ No newline at end of file From 4df3b3a9bb5442071644272e779ac52ca925a3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 10:12:28 +0900 Subject: [PATCH 05/29] feat(domain): check lotto includes bonus number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로또 번호가 보너스 번호를 포함하고 있는지 확인합니다. 해당 로직은 검증에서도 사용되기 떄문에 실제 로직은 util에 두고 domain에서 래핑해서 사용합니다. --- README.md | 2 +- __tests__/DomainTest.js | 12 +++++++++++- src/domains/ranking.js | 7 ++++++- src/utils/array.js | 2 ++ 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/utils/array.js diff --git a/README.md b/README.md index ce62ae960..fc16011f9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - [ ] 구매량 만큼의 로또 데이터를 생성한다. - [ ] `당첨 번호와 로또 번호를 비교/채점하는 계산` - [x] 당첨 번호와 일치하는 번호의 수를 계산한다. - - [ ] 보너스 번호가 당첨 번호와 일치하는지 계산한다. + - [x] 보너스 번호가 로또 번호에 존재하는 지 계산한다. - [ ] 로또의 등수를 계산한다. - [ ] `로또 수익률을 계산` - [ ] 전체 상금을 계산한다. diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js index 03236b142..3f4d6cbce 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/DomainTest.js @@ -1,4 +1,4 @@ -import { calculateMatchCount } from "../src/domains/ranking"; +import { calculateMatchCount, isBonusMatch } from "../src/domains/ranking"; describe("순위 결정 관련 비지니스 로직", () => { test("일치하는 개수를 반환한다.", () => { const ticket = [1, 2, 3, 4, 5, 6]; @@ -10,4 +10,14 @@ describe("순위 결정 관련 비지니스 로직", () => { const winning = [7, 8, 9, 10, 11, 12]; expect(calculateMatchCount(ticket, winning)).toBe(0); }); + test("보너스 넘버를 포함하고 있다면 true를 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const bonusInclude = 5 + expect(isBonusMatch(ticket, bonusInclude)).toBe(true); + }); + test("보너스 넘버를 포함하고 있지않다면 false를 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const bonusNotInclude = 45; + expect(isBonusMatch(ticket, bonusNotInclude)).toBe(false); + }) }) \ No newline at end of file diff --git a/src/domains/ranking.js b/src/domains/ranking.js index cd38837e5..6b7a3d8d5 100644 --- a/src/domains/ranking.js +++ b/src/domains/ranking.js @@ -1,4 +1,9 @@ +import { isArrayIncludeNum } from "../utils/array"; export function calculateMatchCount(ticket, winning, numbersPerTicket = 6) { const uniqueNumbers = new Set([...ticket, ...winning]); return numbersPerTicket * 2 - uniqueNumbers.size; -} \ No newline at end of file +} + +export function isBonusMatch(ticket, bonusNum) { + return isArrayIncludeNum(ticket, bonusNum); +}; \ No newline at end of file diff --git a/src/utils/array.js b/src/utils/array.js new file mode 100644 index 000000000..2b6c30457 --- /dev/null +++ b/src/utils/array.js @@ -0,0 +1,2 @@ +// 재사용을 하기에 util로 분리 +export const isArrayIncludeNum = (arr, num) => arr.includes(num); \ No newline at end of file From 467beb03f0e86d5ab568f82107bab04a438e5a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 10:15:28 +0900 Subject: [PATCH 06/29] style: apply blank line after imports across files --- __tests__/DomainTest.js | 1 + src/domains/ranking.js | 1 + 2 files changed, 2 insertions(+) diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js index 3f4d6cbce..c4cc41d9f 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/DomainTest.js @@ -1,4 +1,5 @@ import { calculateMatchCount, isBonusMatch } from "../src/domains/ranking"; + describe("순위 결정 관련 비지니스 로직", () => { test("일치하는 개수를 반환한다.", () => { const ticket = [1, 2, 3, 4, 5, 6]; diff --git a/src/domains/ranking.js b/src/domains/ranking.js index 6b7a3d8d5..e4a1ea86e 100644 --- a/src/domains/ranking.js +++ b/src/domains/ranking.js @@ -1,4 +1,5 @@ import { isArrayIncludeNum } from "../utils/array"; + export function calculateMatchCount(ticket, winning, numbersPerTicket = 6) { const uniqueNumbers = new Set([...ticket, ...winning]); return numbersPerTicket * 2 - uniqueNumbers.size; From 91d56173b73e93e7583d61c2f1b92fcc79a0d7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 11:09:47 +0900 Subject: [PATCH 07/29] feat(domain): compute lotto rank based on match count and bonus flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일치하는 숫자의 개수와 보너스 여부를 가지고 등수를 결정합니다. 랭크 테이블을 상수로 추가 정의하여서 등수 탐색합니다. --- README.md | 4 ++-- __tests__/DomainTest.js | 15 +++++++++++++-- src/constants.js | 11 +++++++++++ src/domains/ranking.js | 11 ++++++++--- src/utils/array.js | 2 +- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fc16011f9..feadf0242 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ - [ ] `구매량 만큼 로또를 만드는 계산` - [ ] <당첨 번호 6개> 의 데이터를 생성한다. - [ ] 구매량 만큼의 로또 데이터를 생성한다. -- [ ] `당첨 번호와 로또 번호를 비교/채점하는 계산` +- [x] `당첨 번호와 로또 번호를 비교/채점하는 계산` - [x] 당첨 번호와 일치하는 번호의 수를 계산한다. - [x] 보너스 번호가 로또 번호에 존재하는 지 계산한다. - - [ ] 로또의 등수를 계산한다. + - [x] 로또의 등수를 계산한다. - [ ] `로또 수익률을 계산` - [ ] 전체 상금을 계산한다. - [ ] 수익률을 계산한다. diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js index c4cc41d9f..4105298bf 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/DomainTest.js @@ -1,6 +1,7 @@ -import { calculateMatchCount, isBonusMatch } from "../src/domains/ranking"; +import { calculateMatchCount, isBonusMatch, determineRankOf } from "../src/domains/ranking"; +import { RANK_TABLE } from "../src/constants"; -describe("순위 결정 관련 비지니스 로직", () => { +describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { test("일치하는 개수를 반환한다.", () => { const ticket = [1, 2, 3, 4, 5, 6]; const winning = [1, 2, 3, 4, 5, 6]; @@ -20,5 +21,15 @@ describe("순위 결정 관련 비지니스 로직", () => { const ticket = [1, 2, 3, 4, 5, 6]; const bonusNotInclude = 45; expect(isBonusMatch(ticket, bonusNotInclude)).toBe(false); + }); + test("일치하는 번호의 수가 5개이고 보너스 번호가 일치하면 2등이다.", () => { + const matchedCnt = 5; + const isBonusMatch = true; + expect(determineRankOf(matchedCnt, isBonusMatch, RANK_TABLE)).toBe(2); + }) + test("일치하는 번호의 수가 5개이고 보너스 번호가 일치하지 않으면 3등이다.", () => { + const matchedCnt = 5; + const isBonusMatch = false; + expect(determineRankOf(matchedCnt, isBonusMatch, RANK_TABLE)).toBe(3); }) }) \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index f77f827bc..2d1c9a871 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,10 +5,21 @@ export const LOTTO_CONSTANTS = Object.freeze({ NUMBERS_PER_TICKET: 6, }); +export const RANK_TABLE = Object.freeze({ // 일치하는 숫자 개수 : { bonusTrue : 랭크, bonusFalse: 랭크} + 6: Object.freeze({true: 1, false: 2,}), + 5: Object.freeze({true: 2, false: 3,}), + 4: Object.freeze({true: 4, false: 4,}), + 3: Object.freeze({true: 5, false: 5,}), + 2: Object.freeze({true: 6, false: 6,}), + 1: Object.freeze({true: 6, false: 6,}), + 0: Object.freeze({true: 6, false: 6,}), +}); + export const PRIZE_TABLE = Object.freeze({ 1: 2_000_000_000, 2: 30_000_000, // 5개 일치 + 보너스 번호 일치 3: 1_500_000, 4: 50_000, 5: 5_000, + 6: 0, }); \ No newline at end of file diff --git a/src/domains/ranking.js b/src/domains/ranking.js index e4a1ea86e..75d86b763 100644 --- a/src/domains/ranking.js +++ b/src/domains/ranking.js @@ -1,10 +1,15 @@ -import { isArrayIncludeNum } from "../utils/array"; +import { includesNumber } from "../utils/array"; export function calculateMatchCount(ticket, winning, numbersPerTicket = 6) { const uniqueNumbers = new Set([...ticket, ...winning]); return numbersPerTicket * 2 - uniqueNumbers.size; -} +}; export function isBonusMatch(ticket, bonusNum) { - return isArrayIncludeNum(ticket, bonusNum); + return includesNumber(ticket, bonusNum); +}; + +export function determineRankOf(matchedCnt, bonusFlag, RANK_TABLE) { + const rank = RANK_TABLE[matchedCnt][bonusFlag]; + return rank; }; \ No newline at end of file diff --git a/src/utils/array.js b/src/utils/array.js index 2b6c30457..7adcbc81f 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -1,2 +1,2 @@ // 재사용을 하기에 util로 분리 -export const isArrayIncludeNum = (arr, num) => arr.includes(num); \ No newline at end of file +export const includesNumber = (arr, num) => arr.includes(num); \ No newline at end of file From e252496d0193b053e27d4ce67aa6e5258408f151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 12:00:28 +0900 Subject: [PATCH 08/29] feat(domain): accumulate profit of lotto based on rank MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상금 테이블을 통해서 랭크에 따른 수익 누적합을 계산합니다. --- README.md | 2 +- __tests__/DomainTest.js | 16 +++++++++++++++- src/domains/profit.js | 7 +++++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/domains/profit.js diff --git a/README.md b/README.md index feadf0242..7748253f6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - [x] 보너스 번호가 로또 번호에 존재하는 지 계산한다. - [x] 로또의 등수를 계산한다. - [ ] `로또 수익률을 계산` - - [ ] 전체 상금을 계산한다. + - [x] 전체 상금을 계산한다. - [ ] 수익률을 계산한다. ### Effect(부수효과 액션: 입출력/난수/에러) diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js index 4105298bf..f823abc34 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/DomainTest.js @@ -1,5 +1,6 @@ import { calculateMatchCount, isBonusMatch, determineRankOf } from "../src/domains/ranking"; -import { RANK_TABLE } from "../src/constants"; +import { accumulateProfit } from "../src/domains/profit"; +import { RANK_TABLE, PRIZE_TABLE } from "../src/constants"; describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { test("일치하는 개수를 반환한다.", () => { @@ -32,4 +33,17 @@ describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { const isBonusMatch = false; expect(determineRankOf(matchedCnt, isBonusMatch, RANK_TABLE)).toBe(3); }) +}); + +describe("당첨 금액 관련 로직 단위 테스트", () => { + test("1등, 4등을 했을 때 2,000,050,000을 반환한다.", () => { + const rankOflottos = [1, 4] + const profit = 2_000_050_000 // 1등(2억) + 4등(5만) + expect(accumulateProfit(rankOflottos, PRIZE_TABLE)).toBe(profit); + }); + test("6등을 했을 떄는 0을 반환한다.", () => { + const rank_6th = [6, 6, 6]; + const profit = 0; + expect(accumulateProfit(rank_6th, PRIZE_TABLE)).toBe(profit); + }) }) \ No newline at end of file diff --git a/src/domains/profit.js b/src/domains/profit.js new file mode 100644 index 000000000..45e68e51f --- /dev/null +++ b/src/domains/profit.js @@ -0,0 +1,7 @@ +export function accumulateProfit(rankArr, PRIZE_TABLE) { + const totalProfit = rankArr.reduce( + (profit, rank) => profit + PRIZE_TABLE[rank], + 0 + ); + return totalProfit; +}; From 2f25d741aa453aa9ba1354b220167eca2207d8d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 13:01:40 +0900 Subject: [PATCH 09/29] feat(domain): implement ROI percent calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수익률을 퍼센트로 계산하는 함수를 구현하였습니다. 소수점을 처리하는 부분은 출력에 대한 표현으로 정의하고 퍼센트 연산의 정확한 결과를 반환합니다. 추후 확장되어 해당 값을 가지고 다른 계산이 필요할 때 정확한 연산을 할 수 있습니다. --- README.md | 4 ++-- __tests__/DomainTest.js | 15 +++++++++++++-- src/domains/profit.js | 8 ++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7748253f6..fe91ee320 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ - [x] 당첨 번호와 일치하는 번호의 수를 계산한다. - [x] 보너스 번호가 로또 번호에 존재하는 지 계산한다. - [x] 로또의 등수를 계산한다. -- [ ] `로또 수익률을 계산` +- [x] `로또 수익률을 계산` - [x] 전체 상금을 계산한다. - - [ ] 수익률을 계산한다. + - [x] 퍼센트 수익률을 계산한다. ### Effect(부수효과 액션: 입출력/난수/에러) - [ ] `사용자의 입력 액션` diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js index f823abc34..01162bb1e 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/DomainTest.js @@ -1,5 +1,5 @@ import { calculateMatchCount, isBonusMatch, determineRankOf } from "../src/domains/ranking"; -import { accumulateProfit } from "../src/domains/profit"; +import { accumulateProfit, getRateOfInvestmentByPercent } from "../src/domains/profit"; import { RANK_TABLE, PRIZE_TABLE } from "../src/constants"; describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { @@ -45,5 +45,16 @@ describe("당첨 금액 관련 로직 단위 테스트", () => { const rank_6th = [6, 6, 6]; const profit = 0; expect(accumulateProfit(rank_6th, PRIZE_TABLE)).toBe(profit); - }) + }); + test("퍼센트 환산된 수익률을 반환한다.", () => { + // 2000 / 1000 = 2 -> 200% + expect(getRateOfInvestmentByPercent(2000, 1000)).toBe(200); + // 995 / 1000 = 0.995 -> 99.5% + expect(getRateOfInvestmentByPercent(995, 1000)).toBe(99.5); + }); + test("소수점 이하의 결과를 정확히 반환한다.", () => { + // 3333 / 7000 = 0.47614... -> 47.614..% + const expectAboutResult = 47.614 + expect(getRateOfInvestmentByPercent(3333, 7000)).toBeCloseTo(expectAboutResult); + }); }) \ No newline at end of file diff --git a/src/domains/profit.js b/src/domains/profit.js index 45e68e51f..537409b22 100644 --- a/src/domains/profit.js +++ b/src/domains/profit.js @@ -5,3 +5,11 @@ export function accumulateProfit(rankArr, PRIZE_TABLE) { ); return totalProfit; }; + +// 출력 요구사항에 "수익률은 소수점 둘째 자리에서 반올림한다"를 통해 반올림만 명시 +// -> 퍼센트 변환는 계산, 반올림은 표현의 영역이라고 판단 +export function getRateOfInvestmentByPercent(totalProfit, investment) { + const ratio = totalProfit / investment; + const ratioByPercent = ratio * 100; + return ratioByPercent; +} From 8827cc36b1edff46cbf55c773684ae5e56505b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 14:12:00 +0900 Subject: [PATCH 10/29] test: seperate to specific domain test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 도메인 테스트 파일을 각 도메인별 유닛 테스트 파일로 분리하였습니다. --- __tests__/ProfitDomainUnit.test.js | 26 +++++++++++++++++++ ...omainTest.js => RankingDomainUnit.test.js} | 26 +------------------ __tests__/ValidateDomainUnit.test.js | 15 +++++++++++ 3 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 __tests__/ProfitDomainUnit.test.js rename __tests__/{DomainTest.js => RankingDomainUnit.test.js} (62%) create mode 100644 __tests__/ValidateDomainUnit.test.js diff --git a/__tests__/ProfitDomainUnit.test.js b/__tests__/ProfitDomainUnit.test.js new file mode 100644 index 000000000..2ab2e74d0 --- /dev/null +++ b/__tests__/ProfitDomainUnit.test.js @@ -0,0 +1,26 @@ +import { accumulateProfit, getRateOfInvestmentByPercent } from "../src/domains/profit"; +import { PRIZE_TABLE } from "../src/constants"; + +describe("로또 수익 관련 로직 단위 테스트", () => { + test("1등, 4등을 했을 때 2,000,050,000을 반환한다.", () => { + const rankOflottos = [1, 4] + const profit = 2_000_050_000 // 1등(2억) + 4등(5만) + expect(accumulateProfit(rankOflottos, PRIZE_TABLE)).toBe(profit); + }); + test("6등을 했을 떄는 0을 반환한다.", () => { + const rank_6th = [6, 6, 6]; + const profit = 0; + expect(accumulateProfit(rank_6th, PRIZE_TABLE)).toBe(profit); + }); + test("퍼센트 환산된 수익률을 반환한다.", () => { + // 2000 / 1000 = 2 -> 200% + expect(getRateOfInvestmentByPercent(2000, 1000)).toBe(200); + // 995 / 1000 = 0.995 -> 99.5% + expect(getRateOfInvestmentByPercent(995, 1000)).toBe(99.5); + }); + test("소수점 이하의 결과를 정확히 반환한다.", () => { + // 3333 / 7000 = 0.47614... -> 47.614..% + const expectAboutResult = 47.614 + expect(getRateOfInvestmentByPercent(3333, 7000)).toBeCloseTo(expectAboutResult); + }); +}) \ No newline at end of file diff --git a/__tests__/DomainTest.js b/__tests__/RankingDomainUnit.test.js similarity index 62% rename from __tests__/DomainTest.js rename to __tests__/RankingDomainUnit.test.js index 01162bb1e..0a4b8dd6c 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/RankingDomainUnit.test.js @@ -33,28 +33,4 @@ describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { const isBonusMatch = false; expect(determineRankOf(matchedCnt, isBonusMatch, RANK_TABLE)).toBe(3); }) -}); - -describe("당첨 금액 관련 로직 단위 테스트", () => { - test("1등, 4등을 했을 때 2,000,050,000을 반환한다.", () => { - const rankOflottos = [1, 4] - const profit = 2_000_050_000 // 1등(2억) + 4등(5만) - expect(accumulateProfit(rankOflottos, PRIZE_TABLE)).toBe(profit); - }); - test("6등을 했을 떄는 0을 반환한다.", () => { - const rank_6th = [6, 6, 6]; - const profit = 0; - expect(accumulateProfit(rank_6th, PRIZE_TABLE)).toBe(profit); - }); - test("퍼센트 환산된 수익률을 반환한다.", () => { - // 2000 / 1000 = 2 -> 200% - expect(getRateOfInvestmentByPercent(2000, 1000)).toBe(200); - // 995 / 1000 = 0.995 -> 99.5% - expect(getRateOfInvestmentByPercent(995, 1000)).toBe(99.5); - }); - test("소수점 이하의 결과를 정확히 반환한다.", () => { - // 3333 / 7000 = 0.47614... -> 47.614..% - const expectAboutResult = 47.614 - expect(getRateOfInvestmentByPercent(3333, 7000)).toBeCloseTo(expectAboutResult); - }); -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/__tests__/ValidateDomainUnit.test.js b/__tests__/ValidateDomainUnit.test.js new file mode 100644 index 000000000..e7745a188 --- /dev/null +++ b/__tests__/ValidateDomainUnit.test.js @@ -0,0 +1,15 @@ +describe("로또 도메인 검증", () => { + test("로또 번호는 6개이어야 한다.", () => { + expect(() => validateLottoNumbers([1, 2, 3, 4, 5])).toThrow(); + }); + test("각 로또 번호는 1~45 범위 안에 있어야 한다.", () => { + expect(() => validateLottoNumbers([0, 1, 2, 3, 4, 5])).toThrow(); // < 1 + expect(() => validateLottoNumbers([1, 2, 3, 4, 5, 46])).toThrow(); // > 45 + }); + test("로또 번호는 중복될 수 없다.", () => { + expect(() => validateLottoNumbers([1, 2, 3, 4, 5, 5])).toThrow(); + }); + test("정상적인 로또 번호는 에러를 발생시키지 않는다.", () => { + expect(() => validateLottoNumbers([1, 2, 3, 4, 5, 45])).not.toThrow(); + }) +}) \ No newline at end of file From 15dc33e1b490af1b2b6a50dc5f98ff0a9a995602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 15:17:13 +0900 Subject: [PATCH 11/29] feat(domain): implement validate lotto number in range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로또 번호가 1~45 범위인지를 검증하는 함수를 구현하였습니다. --- README.md | 2 +- __tests__/RankingDomainUnit.test.js | 3 +-- __tests__/ValidateDomainUnit.test.js | 13 ++++++++----- src/constants.js | 8 +++++++- src/domains/lottoRules.js | 9 +++++++++ src/domains/validate.js | 7 +++++++ 6 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 src/domains/lottoRules.js create mode 100644 src/domains/validate.js diff --git a/README.md b/README.md index fe91ee320..ad025663a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - [ ] 보너스 번호 문자열을 숫자 타입으로 변환한다. - [ ] `사용자의 입력값 검증 계산` - [ ] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. - - [ ] 로또 당첨 번호가 6개인지 검증한다. + - [x] 로또 당첨 번호가 6개인지 검증한다. - [ ] 로또 당첨 번호가 중복되지 않는 지 검증한다. - [ ] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [ ] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. diff --git a/__tests__/RankingDomainUnit.test.js b/__tests__/RankingDomainUnit.test.js index 0a4b8dd6c..9de6319bb 100644 --- a/__tests__/RankingDomainUnit.test.js +++ b/__tests__/RankingDomainUnit.test.js @@ -1,6 +1,5 @@ import { calculateMatchCount, isBonusMatch, determineRankOf } from "../src/domains/ranking"; -import { accumulateProfit, getRateOfInvestmentByPercent } from "../src/domains/profit"; -import { RANK_TABLE, PRIZE_TABLE } from "../src/constants"; +import { RANK_TABLE } from "../src/constants"; describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { test("일치하는 개수를 반환한다.", () => { diff --git a/__tests__/ValidateDomainUnit.test.js b/__tests__/ValidateDomainUnit.test.js index e7745a188..3ea745174 100644 --- a/__tests__/ValidateDomainUnit.test.js +++ b/__tests__/ValidateDomainUnit.test.js @@ -1,15 +1,18 @@ +import { validateLottoNumbers } from "../src/domains/validate"; +import { ERROR_MSG } from "../src/constants"; + describe("로또 도메인 검증", () => { test("로또 번호는 6개이어야 한다.", () => { - expect(() => validateLottoNumbers([1, 2, 3, 4, 5])).toThrow(); + expect(validateLottoNumbers([1, 2, 3, 4, 5])).toBe(ERROR_MSG.LOTTO_SIZE); }); test("각 로또 번호는 1~45 범위 안에 있어야 한다.", () => { - expect(() => validateLottoNumbers([0, 1, 2, 3, 4, 5])).toThrow(); // < 1 - expect(() => validateLottoNumbers([1, 2, 3, 4, 5, 46])).toThrow(); // > 45 + expect(validateLottoNumbers([0, 1, 2, 3, 4, 5])).toBe(ERROR_MSG.LOTTO_NUM_RANGE); // < 1 + expect(validateLottoNumbers([1, 2, 3, 4, 5, 46])).toBe(ERROR_MSG.LOTTO_NUM_RANGE); // > 45 }); test("로또 번호는 중복될 수 없다.", () => { - expect(() => validateLottoNumbers([1, 2, 3, 4, 5, 5])).toThrow(); + expect(validateLottoNumbers([1, 2, 3, 4, 5, 5])).toBe(ERROR_MSG.LOTTO_NUM_UNIQUE); }); test("정상적인 로또 번호는 에러를 발생시키지 않는다.", () => { - expect(() => validateLottoNumbers([1, 2, 3, 4, 5, 45])).not.toThrow(); + expect(validateLottoNumbers([1, 2, 3, 4, 5, 45])).toBe(true); }) }) \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 2d1c9a871..54a926ada 100644 --- a/src/constants.js +++ b/src/constants.js @@ -22,4 +22,10 @@ export const PRIZE_TABLE = Object.freeze({ 4: 50_000, 5: 5_000, 6: 0, -}); \ No newline at end of file +}); + +export const ERROR_MSG = Object.freeze({ + LOTTO_SIZE: "[ERROR] 로또 번호는 6개여야 합니다.", + LOTTO_NUM_RANGE: "[ERROR] 로또 번호는 1~45만으로 이루어집니다.", + LOTTO_NUM_UNIQUE: "[ERROR] 하나의 로또에 중복된 숫자가 존재할 수 없습니다.", +}) \ No newline at end of file diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js new file mode 100644 index 000000000..074bf1679 --- /dev/null +++ b/src/domains/lottoRules.js @@ -0,0 +1,9 @@ +import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants" + +export const inRange = (arr) => + arr.every( // every: 함수형, 즉시 종료 + n => + n >= LOTTO_CONSTANTS.MIN_NUMBER && // 1 + n <= LOTTO_CONSTANTS.MAX_NUMBER // 45 + ) || ERROR_MSG.LOTTO_NUM_RANGE; + diff --git a/src/domains/validate.js b/src/domains/validate.js new file mode 100644 index 000000000..c4b0b8d9a --- /dev/null +++ b/src/domains/validate.js @@ -0,0 +1,7 @@ +import { inRange } from "./lottoRules"; + +export function validateLottoNumbers(lottoArr) { + const r = inRange(lottoArr); + if(r !== true) return r + return true; +} \ No newline at end of file From c07e8c468133629f95699bb151ac5bde6767f88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 15:49:11 +0900 Subject: [PATCH 12/29] feat(domain): implement validate lotto num is unique MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set를 이용하여 로또 번호과 중복되지 않음을 검증합니다. --- README.md | 2 +- src/domains/lottoRules.js | 4 ++++ src/domains/validate.js | 10 ++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ad025663a..00316b038 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [ ] `사용자의 입력값 검증 계산` - [ ] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. - [x] 로또 당첨 번호가 6개인지 검증한다. - - [ ] 로또 당첨 번호가 중복되지 않는 지 검증한다. + - [x] 로또 당첨 번호가 중복되지 않는 지 검증한다. - [ ] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [ ] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [ ] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js index 074bf1679..b8ec98aad 100644 --- a/src/domains/lottoRules.js +++ b/src/domains/lottoRules.js @@ -7,3 +7,7 @@ export const inRange = (arr) => n <= LOTTO_CONSTANTS.MAX_NUMBER // 45 ) || ERROR_MSG.LOTTO_NUM_RANGE; +export const isUnique = (arr) => + new Set(arr).size === arr.length || ERROR_MSG.LOTTO_NUM_UNIQUE; + +export const lottoRules = [inRange, isUnique]; diff --git a/src/domains/validate.js b/src/domains/validate.js index c4b0b8d9a..e5168c090 100644 --- a/src/domains/validate.js +++ b/src/domains/validate.js @@ -1,7 +1,9 @@ -import { inRange } from "./lottoRules"; +import { lottoRules } from "./lottoRules"; export function validateLottoNumbers(lottoArr) { - const r = inRange(lottoArr); - if(r !== true) return r - return true; + for(const rule of lottoRules) { + const result = rule(lottoArr); + if(result !== true) return result; // err_msg + } + return true; // 모두 통과 } \ No newline at end of file From 03fba68768bf3942ae91807cd8363009e73f6e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 17:13:20 +0900 Subject: [PATCH 13/29] feat(domain): implement validate lotto size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로또 사이즈(6)와 일치하는 지 검증하는 함수를 구현하였습니다. --- README.md | 2 +- src/domains/lottoRules.js | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 00316b038..34f82dce6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - [ ] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. - [x] 로또 당첨 번호가 6개인지 검증한다. - [x] 로또 당첨 번호가 중복되지 않는 지 검증한다. - - [ ] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. + - [x] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [ ] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [ ] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. - [ ] `구매량 만큼 로또를 만드는 계산` diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js index b8ec98aad..e172320d1 100644 --- a/src/domains/lottoRules.js +++ b/src/domains/lottoRules.js @@ -1,13 +1,16 @@ import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants" +export const hasExactSize = (arr) => + arr.length === LOTTO_CONSTANTS.NUMBERS_PER_TICKET || ERROR_MSG.LOTTO_SIZE + export const inRange = (arr) => arr.every( // every: 함수형, 즉시 종료 n => - n >= LOTTO_CONSTANTS.MIN_NUMBER && // 1 - n <= LOTTO_CONSTANTS.MAX_NUMBER // 45 + n >= LOTTO_CONSTANTS.MIN_NUMBER && + n <= LOTTO_CONSTANTS.MAX_NUMBER ) || ERROR_MSG.LOTTO_NUM_RANGE; export const isUnique = (arr) => new Set(arr).size === arr.length || ERROR_MSG.LOTTO_NUM_UNIQUE; -export const lottoRules = [inRange, isUnique]; +export const lottoRules = [hasExactSize, inRange, isUnique]; From 10be6a89bbdf7197aeef7ae8b5c464bb676e0444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 17:48:58 +0900 Subject: [PATCH 14/29] feat(domain): implement validate bonus number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보너스 번호가 1~45 사이의 숫자인지, 당첨 번호와 중복되지 않는지 검증합니다. --- README.md | 4 ++-- __tests__/ValidateDomainUnit.test.js | 13 +++++++++++-- src/domains/lottoRules.js | 16 ++++++++++++---- src/domains/validate.js | 10 +++++++++- src/utils/array.js | 7 ++++++- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 34f82dce6..af27436fd 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ - [x] 로또 당첨 번호가 6개인지 검증한다. - [x] 로또 당첨 번호가 중복되지 않는 지 검증한다. - [x] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - - [ ] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - - [ ] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. + - [x] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. + - [x] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. - [ ] `구매량 만큼 로또를 만드는 계산` - [ ] <당첨 번호 6개> 의 데이터를 생성한다. - [ ] 구매량 만큼의 로또 데이터를 생성한다. diff --git a/__tests__/ValidateDomainUnit.test.js b/__tests__/ValidateDomainUnit.test.js index 3ea745174..862121a00 100644 --- a/__tests__/ValidateDomainUnit.test.js +++ b/__tests__/ValidateDomainUnit.test.js @@ -1,7 +1,7 @@ -import { validateLottoNumbers } from "../src/domains/validate"; +import { validateLottoNumbers, validateBonusNumber } from "../src/domains/validate"; import { ERROR_MSG } from "../src/constants"; -describe("로또 도메인 검증", () => { +describe("당첨 번호 도메인 검증", () => { test("로또 번호는 6개이어야 한다.", () => { expect(validateLottoNumbers([1, 2, 3, 4, 5])).toBe(ERROR_MSG.LOTTO_SIZE); }); @@ -14,5 +14,14 @@ describe("로또 도메인 검증", () => { }); test("정상적인 로또 번호는 에러를 발생시키지 않는다.", () => { expect(validateLottoNumbers([1, 2, 3, 4, 5, 45])).toBe(true); + }); +}); + +describe("보너스 번호 도메인 검증", () => { + test("보너스 번호는 1~45 범위 안에 있어야 한다.", () => { + expect(validateBonusNumber(46)).toBe(ERROR_MSG.LOTTO_NUM_RANGE); + }); + test("보너스 번호는 로또 번호와 중복될 수 없다.", () => { + expect(validateBonusNumber(4, [1, 2, 3, 4, 5, 6])).toBe(ERROR_MSG.LOTTO_NUM_UNIQUE); }) }) \ No newline at end of file diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js index e172320d1..c1e8bfc66 100644 --- a/src/domains/lottoRules.js +++ b/src/domains/lottoRules.js @@ -1,16 +1,24 @@ import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants" +import { toArray, includesNumber } from "../utils/array"; +// 로또의 비지니스 규칙과 관련된 검증들 export const hasExactSize = (arr) => arr.length === LOTTO_CONSTANTS.NUMBERS_PER_TICKET || ERROR_MSG.LOTTO_SIZE -export const inRange = (arr) => - arr.every( // every: 함수형, 즉시 종료 +export const inRange = (valueOrArr) => { + const arr = toArray(valueOrArr); + return arr.every( // every: 함수형, 즉시 종료 n => n >= LOTTO_CONSTANTS.MIN_NUMBER && n <= LOTTO_CONSTANTS.MAX_NUMBER ) || ERROR_MSG.LOTTO_NUM_RANGE; +} -export const isUnique = (arr) => +export const isLottoNumUnique = (arr) => new Set(arr).size === arr.length || ERROR_MSG.LOTTO_NUM_UNIQUE; -export const lottoRules = [hasExactSize, inRange, isUnique]; +export const isBonusUnique = (num, arr) => + !includesNumber(arr, num) || ERROR_MSG.LOTTO_NUM_UNIQUE; + +export const lottoRules = [hasExactSize, inRange, isLottoNumUnique]; +export const bonusRules = [inRange, isBonusUnique]; diff --git a/src/domains/validate.js b/src/domains/validate.js index e5168c090..92d84b7b2 100644 --- a/src/domains/validate.js +++ b/src/domains/validate.js @@ -1,4 +1,4 @@ -import { lottoRules } from "./lottoRules"; +import { lottoRules, bonusRules } from "./lottoRules"; export function validateLottoNumbers(lottoArr) { for(const rule of lottoRules) { @@ -6,4 +6,12 @@ export function validateLottoNumbers(lottoArr) { if(result !== true) return result; // err_msg } return true; // 모두 통과 +} + +export function validateBonusNumber(lottoArr, bonusNum) { + for(const rule of bonusRules) { + const result = rule(lottoArr, bonusNum); + if(result !== true) return result; + } + return true; } \ No newline at end of file diff --git a/src/utils/array.js b/src/utils/array.js index 7adcbc81f..6e96ee9eb 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -1,2 +1,7 @@ // 재사용을 하기에 util로 분리 -export const includesNumber = (arr, num) => arr.includes(num); \ No newline at end of file +export const includesNumber = (arr, num) => arr.includes(num); + +export const toArray = (value) => { + if(Array.isArray(value)) return value; + return [value]; +} \ No newline at end of file From 6baad1f96fe43007b2ee63e42dad5db9da468501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 18:07:20 +0900 Subject: [PATCH 15/29] feat(domain): implement validate cost --- README.md | 4 ++-- __tests__/ValidateDomainUnit.test.js | 16 ++++++++++++++-- src/constants.js | 1 + src/domains/lottoRules.js | 5 +++++ src/domains/validate.js | 14 +++++++++++--- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index af27436fd..c3ef7f1cd 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ - [ ] 구입 금액을 로또 1장의 가격으로 나눈다. - [ ] 로또 당첨 번호를 쉼표(,)를 기준으로 나누어 숫자 배열을 생성한다. - [ ] 보너스 번호 문자열을 숫자 타입으로 변환한다. -- [ ] `사용자의 입력값 검증 계산` - - [ ] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. +- [x] `사용자의 입력값 검증 계산` + - [x] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. - [x] 로또 당첨 번호가 6개인지 검증한다. - [x] 로또 당첨 번호가 중복되지 않는 지 검증한다. - [x] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. diff --git a/__tests__/ValidateDomainUnit.test.js b/__tests__/ValidateDomainUnit.test.js index 862121a00..61b89f38a 100644 --- a/__tests__/ValidateDomainUnit.test.js +++ b/__tests__/ValidateDomainUnit.test.js @@ -1,4 +1,4 @@ -import { validateLottoNumbers, validateBonusNumber } from "../src/domains/validate"; +import { validateLottoNumbers, validateBonusNumber, validateCost } from "../src/domains/validate"; import { ERROR_MSG } from "../src/constants"; describe("당첨 번호 도메인 검증", () => { @@ -12,7 +12,7 @@ describe("당첨 번호 도메인 검증", () => { test("로또 번호는 중복될 수 없다.", () => { expect(validateLottoNumbers([1, 2, 3, 4, 5, 5])).toBe(ERROR_MSG.LOTTO_NUM_UNIQUE); }); - test("정상적인 로또 번호는 에러를 발생시키지 않는다.", () => { + test("정상적인 로또 번호는 true를 반환한다..", () => { expect(validateLottoNumbers([1, 2, 3, 4, 5, 45])).toBe(true); }); }); @@ -23,5 +23,17 @@ describe("보너스 번호 도메인 검증", () => { }); test("보너스 번호는 로또 번호와 중복될 수 없다.", () => { expect(validateBonusNumber(4, [1, 2, 3, 4, 5, 6])).toBe(ERROR_MSG.LOTTO_NUM_UNIQUE); + }); + test("정상적인 보너스 번호는 true를 반환한다.", () => { + expect(validateBonusNumber(25, [1, 2, 3, 4, 5, 6])).toBe(true); + }) +}); + +describe("구입 금액 도메인 검증", () => { + test("구입 금액은 단위가 1000이어야 한다.", () => { + expect(validateCost(2500)).toBe(ERROR_MSG.COST_UNIT); + }); + test("정상적인 구입 금액은 true를 반환한다.", () => { + expect(validateCost(2000)).toBe(true); }) }) \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 54a926ada..179ea55ea 100644 --- a/src/constants.js +++ b/src/constants.js @@ -25,6 +25,7 @@ export const PRIZE_TABLE = Object.freeze({ }); export const ERROR_MSG = Object.freeze({ + COST_UNIT: "[ERROR] 구입 금액은 1000원 단위의 숫자여야 합니다.", LOTTO_SIZE: "[ERROR] 로또 번호는 6개여야 합니다.", LOTTO_NUM_RANGE: "[ERROR] 로또 번호는 1~45만으로 이루어집니다.", LOTTO_NUM_UNIQUE: "[ERROR] 하나의 로또에 중복된 숫자가 존재할 수 없습니다.", diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js index c1e8bfc66..7bd19db9d 100644 --- a/src/domains/lottoRules.js +++ b/src/domains/lottoRules.js @@ -2,6 +2,9 @@ import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants" import { toArray, includesNumber } from "../utils/array"; // 로또의 비지니스 규칙과 관련된 검증들 +export const isValidPurchaseAmount = (amount) => + amount % LOTTO_CONSTANTS.TICKET_PRICE === 0 || ERROR_MSG.COST_UNIT; + export const hasExactSize = (arr) => arr.length === LOTTO_CONSTANTS.NUMBERS_PER_TICKET || ERROR_MSG.LOTTO_SIZE @@ -20,5 +23,7 @@ export const isLottoNumUnique = (arr) => export const isBonusUnique = (num, arr) => !includesNumber(arr, num) || ERROR_MSG.LOTTO_NUM_UNIQUE; +export const costRules = [isValidPurchaseAmount]; export const lottoRules = [hasExactSize, inRange, isLottoNumUnique]; export const bonusRules = [inRange, isBonusUnique]; + diff --git a/src/domains/validate.js b/src/domains/validate.js index 92d84b7b2..e2d88693e 100644 --- a/src/domains/validate.js +++ b/src/domains/validate.js @@ -1,4 +1,12 @@ -import { lottoRules, bonusRules } from "./lottoRules"; +import { costRules,lottoRules, bonusRules } from "./lottoRules"; + +export function validateCost(cost) { + for(const rule of costRules) { + const result = rule(cost); + if(result !== true) return result; + } + return true; +} export function validateLottoNumbers(lottoArr) { for(const rule of lottoRules) { @@ -8,9 +16,9 @@ export function validateLottoNumbers(lottoArr) { return true; // 모두 통과 } -export function validateBonusNumber(lottoArr, bonusNum) { +export function validateBonusNumber(bonusNum, lottoArr) { for(const rule of bonusRules) { - const result = rule(lottoArr, bonusNum); + const result = rule(bonusNum, lottoArr); if(result !== true) return result; } return true; From 3b9c13b282056f43cd8271d63315534111d18651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 18:24:41 +0900 Subject: [PATCH 16/29] feat(util): implement number string parsing function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문자열을 쉼표를 기준으로 나누어 숫자 배열을 만드는 util함수를 구현했습니다. toNumber() 함수를 분리하여 구입 금액이나 숫자를 입력 받을 때 재사용할 수 있습니다. --- README.md | 6 +++--- __tests__/UtilUnit.test.js | 10 ++++++++++ src/utils/parsing.js | 3 +++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 __tests__/UtilUnit.test.js create mode 100644 src/utils/parsing.js diff --git a/README.md b/README.md index c3ef7f1cd..57a618117 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ # 기능 목록 ### Pure(순수 계산: 계산/검증/도메인) - [ ] `입력값을 필요한 형태로 파싱 계산` - - [ ] 구입 금액 문자열을 숫자 타입으로 변환한다. (재사용성 검토) + - [x] 구입 금액 문자열을 숫자 타입으로 변환한다. (재사용성 검토) - [ ] 구입 금액을 로또 1장의 가격으로 나눈다. - - [ ] 로또 당첨 번호를 쉼표(,)를 기준으로 나누어 숫자 배열을 생성한다. - - [ ] 보너스 번호 문자열을 숫자 타입으로 변환한다. + - [x] 로또 당첨 번호를 쉼표(,)를 기준으로 나누어 숫자 배열을 생성한다. + - [x] 보너스 번호 문자열을 숫자 타입으로 변환한다. - [x] `사용자의 입력값 검증 계산` - [x] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. - [x] 로또 당첨 번호가 6개인지 검증한다. diff --git a/__tests__/UtilUnit.test.js b/__tests__/UtilUnit.test.js new file mode 100644 index 000000000..077f20361 --- /dev/null +++ b/__tests__/UtilUnit.test.js @@ -0,0 +1,10 @@ +import { toNumber, parseToArrayByComma } from "../src/utils/parsing"; + +describe("파싱 유틸 검증", () => { + test("숫자로 이루어진 문자열을 숫자 타입으로 변환한다.", () => { + expect(toNumber("123")).toBe(123); + }); + test("쉼표를 포함하는 문자열을 쉼표로 나누어 숫자 배열로 변환한다.", () => { + expect(parseToArrayByComma("1,2,3")).toStrictEqual([1, 2, 3]); + }) +}) \ No newline at end of file diff --git a/src/utils/parsing.js b/src/utils/parsing.js new file mode 100644 index 000000000..c125aeacc --- /dev/null +++ b/src/utils/parsing.js @@ -0,0 +1,3 @@ +export const toNumber = (str) => Number(str); + +export const parseToArrayByComma = (arr) => arr.split(",").map(n => toNumber(n.trim())); \ No newline at end of file From 21af86cb8ec46cd86eb54a6e66356cad8a3599cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 18:32:17 +0900 Subject: [PATCH 17/29] feat(util): implement divison function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구입 금액으로 부터 티켓의 수를 구하기 위한 나눗셈 함수 구현하였습니다. --- README.md | 4 ++-- __tests__/UtilUnit.test.js | 5 ++++- src/utils/parsing.js | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 57a618117..0dc17e8d7 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ # 기능 목록 ### Pure(순수 계산: 계산/검증/도메인) -- [ ] `입력값을 필요한 형태로 파싱 계산` +- [x] `입력값을 필요한 형태로 파싱 계산` - [x] 구입 금액 문자열을 숫자 타입으로 변환한다. (재사용성 검토) - - [ ] 구입 금액을 로또 1장의 가격으로 나눈다. + - [x] 구입 금액을 로또 1장의 가격으로 나눈다. - [x] 로또 당첨 번호를 쉼표(,)를 기준으로 나누어 숫자 배열을 생성한다. - [x] 보너스 번호 문자열을 숫자 타입으로 변환한다. - [x] `사용자의 입력값 검증 계산` diff --git a/__tests__/UtilUnit.test.js b/__tests__/UtilUnit.test.js index 077f20361..452add5bf 100644 --- a/__tests__/UtilUnit.test.js +++ b/__tests__/UtilUnit.test.js @@ -1,4 +1,4 @@ -import { toNumber, parseToArrayByComma } from "../src/utils/parsing"; +import { toNumber, parseToArrayByComma, devisionNumber} from "../src/utils/parsing"; describe("파싱 유틸 검증", () => { test("숫자로 이루어진 문자열을 숫자 타입으로 변환한다.", () => { @@ -6,5 +6,8 @@ describe("파싱 유틸 검증", () => { }); test("쉼표를 포함하는 문자열을 쉼표로 나누어 숫자 배열로 변환한다.", () => { expect(parseToArrayByComma("1,2,3")).toStrictEqual([1, 2, 3]); + }); + test("구입 금액을 단위(1000)으로 나누어 티켓 장수를 반환하다.", () => { + expect(devisionNumber(5000, 1000)).toBe(5); }) }) \ No newline at end of file diff --git a/src/utils/parsing.js b/src/utils/parsing.js index c125aeacc..db6b60e5c 100644 --- a/src/utils/parsing.js +++ b/src/utils/parsing.js @@ -1,3 +1,5 @@ export const toNumber = (str) => Number(str); -export const parseToArrayByComma = (arr) => arr.split(",").map(n => toNumber(n.trim())); \ No newline at end of file +export const parseToArrayByComma = (arr) => arr.split(",").map(n => toNumber(n.trim())); + +export const devisionNumber = (divdend, divisor) => divdend / divisor; \ No newline at end of file From 99bab77ed267b4dde40c0b9106571f9c1b9e3481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 19:14:42 +0900 Subject: [PATCH 18/29] feat(domain): implement create lotto number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 랜덤 숫자를 생성하는 함수를 인자로 받아 로또 규칙을 준수하는 로또 번호를 생성합니다. --- __tests__/CreatLottoDomain.test.js | 17 +++++++++++++++++ src/domains/createLottoNumbers.js | 7 +++++++ src/utils/random.js | 4 ++++ 3 files changed, 28 insertions(+) create mode 100644 __tests__/CreatLottoDomain.test.js create mode 100644 src/domains/createLottoNumbers.js create mode 100644 src/utils/random.js diff --git a/__tests__/CreatLottoDomain.test.js b/__tests__/CreatLottoDomain.test.js new file mode 100644 index 000000000..41ea7979a --- /dev/null +++ b/__tests__/CreatLottoDomain.test.js @@ -0,0 +1,17 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import { creatOneLotto } from "../src/domains/createLottoNumbers"; +import { randomUniquesInRange } from "../src/utils/random"; + +const mockRandoms = (numbers) => { + MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickUniqueNumbersInRange); +}; + +describe("로또 번호 생성 테스트", () => { + test("1~45 사이의 중복되지 않는 숫자 6개를 생성한다.", () => { + mockRandoms([[1, 2, 3, 4, 44, 45]]); + expect(creatOneLotto(randomUniquesInRange)).toStrictEqual([1, 2, 3, 4, 44, 45]); + }); +}) \ No newline at end of file diff --git a/src/domains/createLottoNumbers.js b/src/domains/createLottoNumbers.js new file mode 100644 index 000000000..525bb0e19 --- /dev/null +++ b/src/domains/createLottoNumbers.js @@ -0,0 +1,7 @@ +import { LOTTO_CONSTANTS } from "../constants" + +export function creatOneLotto(drawUniqueNumbers) { + const {MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET} = LOTTO_CONSTANTS + return drawUniqueNumbers(MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET); +}; + diff --git a/src/utils/random.js b/src/utils/random.js new file mode 100644 index 000000000..2d6832a52 --- /dev/null +++ b/src/utils/random.js @@ -0,0 +1,4 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; + +export const randomUniquesInRange = (min, max, quantity) => + MissionUtils.Random.pickUniqueNumbersInRange(min, max, quantity); \ No newline at end of file From 28bdf34d194a3d8d222a14bc15c065f8822e7697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 19:26:46 +0900 Subject: [PATCH 19/29] feat(domain): implement create lotto numbers based on tickets quantity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로또 티켓 수만큼의 로또 숫자 세트를 생성합니다. --- README.md | 6 +++--- __tests__/CreatLottoDomain.test.js | 16 ++++++++++++++-- src/domains/createLottoNumbers.js | 9 +++++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0dc17e8d7..716208636 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ - [x] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [x] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [x] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. -- [ ] `구매량 만큼 로또를 만드는 계산` - - [ ] <당첨 번호 6개> 의 데이터를 생성한다. - - [ ] 구매량 만큼의 로또 데이터를 생성한다. +- [x] `구매량 만큼 로또를 만드는 계산` + - [x] <당첨 번호 6개> 의 데이터를 생성한다. + - [x] 구매량 만큼의 로또 데이터를 생성한다. - [x] `당첨 번호와 로또 번호를 비교/채점하는 계산` - [x] 당첨 번호와 일치하는 번호의 수를 계산한다. - [x] 보너스 번호가 로또 번호에 존재하는 지 계산한다. diff --git a/__tests__/CreatLottoDomain.test.js b/__tests__/CreatLottoDomain.test.js index 41ea7979a..95ee18f01 100644 --- a/__tests__/CreatLottoDomain.test.js +++ b/__tests__/CreatLottoDomain.test.js @@ -1,5 +1,5 @@ import { MissionUtils } from "@woowacourse/mission-utils"; -import { creatOneLotto } from "../src/domains/createLottoNumbers"; +import { createLottos, creatOneLotto } from "../src/domains/createLottoNumbers"; import { randomUniquesInRange } from "../src/utils/random"; const mockRandoms = (numbers) => { @@ -10,8 +10,20 @@ const mockRandoms = (numbers) => { }; describe("로또 번호 생성 테스트", () => { - test("1~45 사이의 중복되지 않는 숫자 6개를 생성한다.", () => { + test("1~45 사이의 중복되지 않는 숫자 6개를 반환한다.", () => { mockRandoms([[1, 2, 3, 4, 44, 45]]); expect(creatOneLotto(randomUniquesInRange)).toStrictEqual([1, 2, 3, 4, 44, 45]); }); + test("quantity만큼의 로또 세트를 반환한다..", () => { + mockRandoms([ // 6개 모킹 + [1, 2, 3, 4, 44, 45], + [1, 2, 3, 4, 44, 45], + [1, 2, 3, 4, 44, 45], + [1, 2, 3, 4, 44, 45], + [1, 2, 3, 4, 44, 45], + [1, 2, 3, 4, 44, 45], + ]) + // 5개만 생성 확인 + expect(createLottos(5, randomUniquesInRange).length).toBe(5); + }) }) \ No newline at end of file diff --git a/src/domains/createLottoNumbers.js b/src/domains/createLottoNumbers.js index 525bb0e19..b8ce6fde4 100644 --- a/src/domains/createLottoNumbers.js +++ b/src/domains/createLottoNumbers.js @@ -1,7 +1,12 @@ import { LOTTO_CONSTANTS } from "../constants" export function creatOneLotto(drawUniqueNumbers) { - const {MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET} = LOTTO_CONSTANTS - return drawUniqueNumbers(MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET); + const { MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET } = LOTTO_CONSTANTS + return drawUniqueNumbers( MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET ); }; +export function createLottos(quantity, drawUniqueNumbers) { + return Array.from({ length: quantity }, () => + creatOneLotto(drawUniqueNumbers) + ); +}; From b2e5d215ce8609b9d53dd9fc8984dc5a8f9ba096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 20:29:15 +0900 Subject: [PATCH 20/29] feat(entity): implement and create Lotto Class --- __tests__/CreatLottoDomain.test.js | 22 ++++++++++++---------- __tests__/LottoTest.js | 2 +- src/App.js | 7 +++++++ src/Lotto.js | 18 ------------------ src/domains/createLottoNumbers.js | 4 +++- src/entities/Lotto.js | 21 +++++++++++++++++++++ 6 files changed, 44 insertions(+), 30 deletions(-) delete mode 100644 src/Lotto.js create mode 100644 src/entities/Lotto.js diff --git a/__tests__/CreatLottoDomain.test.js b/__tests__/CreatLottoDomain.test.js index 95ee18f01..3c691fc64 100644 --- a/__tests__/CreatLottoDomain.test.js +++ b/__tests__/CreatLottoDomain.test.js @@ -1,6 +1,7 @@ import { MissionUtils } from "@woowacourse/mission-utils"; import { createLottos, creatOneLotto } from "../src/domains/createLottoNumbers"; import { randomUniquesInRange } from "../src/utils/random"; +import Lotto from "../src/entities/Lotto"; const mockRandoms = (numbers) => { MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); @@ -11,18 +12,19 @@ const mockRandoms = (numbers) => { describe("로또 번호 생성 테스트", () => { test("1~45 사이의 중복되지 않는 숫자 6개를 반환한다.", () => { - mockRandoms([[1, 2, 3, 4, 44, 45]]); - expect(creatOneLotto(randomUniquesInRange)).toStrictEqual([1, 2, 3, 4, 44, 45]); + const expectedLotto = [1, 2, 3, 4, 44, 45]; + + mockRandoms([expectedLotto]); + const lottoInstance = creatOneLotto(randomUniquesInRange); + + expect(lottoInstance).toBeInstanceOf(Lotto); + expect(lottoInstance.numbers).toEqual(expectedLotto); }); test("quantity만큼의 로또 세트를 반환한다..", () => { - mockRandoms([ // 6개 모킹 - [1, 2, 3, 4, 44, 45], - [1, 2, 3, 4, 44, 45], - [1, 2, 3, 4, 44, 45], - [1, 2, 3, 4, 44, 45], - [1, 2, 3, 4, 44, 45], - [1, 2, 3, 4, 44, 45], - ]) + const sample = [1, 2, 3, 4, 44, 45]; + + mockRandoms([ sample, sample, sample, sample, sample, sample ]) // 6개 모킹 + // 5개만 생성 확인 expect(createLottos(5, randomUniquesInRange).length).toBe(5); }) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..08c09bce2 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,4 +1,4 @@ -import Lotto from "../src/Lotto"; +import Lotto from "../src/entities/Lotto"; describe("로또 클래스 테스트", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { diff --git a/src/App.js b/src/App.js index 091aa0a5d..cc7568d93 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,12 @@ +import { createLottos } from "./domains/createLottoNumbers"; +import Lotto from "./entities/Lotto"; + class App { async run() {} + generateLottoObjects(quantity) { + const lottos = createLottos(); + new Lotto + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js deleted file mode 100644 index cb0b1527e..000000000 --- a/src/Lotto.js +++ /dev/null @@ -1,18 +0,0 @@ -class Lotto { - #numbers; - - constructor(numbers) { - this.#validate(numbers); - this.#numbers = numbers; - } - - #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); - } - } - - // TODO: 추가 기능 구현 -} - -export default Lotto; diff --git a/src/domains/createLottoNumbers.js b/src/domains/createLottoNumbers.js index b8ce6fde4..81704f7f3 100644 --- a/src/domains/createLottoNumbers.js +++ b/src/domains/createLottoNumbers.js @@ -1,8 +1,10 @@ import { LOTTO_CONSTANTS } from "../constants" +import Lotto from "../entities/Lotto"; export function creatOneLotto(drawUniqueNumbers) { const { MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET } = LOTTO_CONSTANTS - return drawUniqueNumbers( MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET ); + const numbers = drawUniqueNumbers( MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET ); + return new Lotto(numbers); }; export function createLottos(quantity, drawUniqueNumbers) { diff --git a/src/entities/Lotto.js b/src/entities/Lotto.js new file mode 100644 index 000000000..4ad271d2a --- /dev/null +++ b/src/entities/Lotto.js @@ -0,0 +1,21 @@ +import { validateLottoNumbers } from "../domains/validate"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + const validateResult = validateLottoNumbers(numbers); + if(validateResult !== true) throw new Error(validateResult); + } + + get numbers() { + return this.#numbers; + } +} + +export default Lotto; From b3cf446b2b2432ef36ececdfa40860bf0911039a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 21:30:38 +0900 Subject: [PATCH 21/29] feat(app): implement read until valid function --- README.md | 12 ++++---- __tests__/ProfitDomainUnit.test.js | 2 +- __tests__/RankingDomainUnit.test.js | 2 +- __tests__/ValidateDomainUnit.test.js | 2 +- src/App.js | 35 +++++++++++++++++++----- src/askUntilValid.js | 14 ++++++++++ src/constants/ioMsg.js | 5 ++++ src/{constants.js => constants/lotto.js} | 0 src/domains/createLottoNumbers.js | 2 +- src/domains/lottoRules.js | 2 +- 10 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 src/askUntilValid.js create mode 100644 src/constants/ioMsg.js rename src/{constants.js => constants/lotto.js} (100%) diff --git a/README.md b/README.md index 716208636..61f55f570 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,12 @@ - [ ] 보너스 번호를 읽는다. - [ ] `에러 발생 액션` - [ ] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. -- [ ] `사용자에게 재입력 액션` - - [ ] 로또 구입 금액에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. - - [ ] 당첨 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. - - [ ] 보너스 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. -- [ ] `난수를 생성 액션` - - [ ] 1 <= n <= 45 범위의 난수 n을 생성한다. +- [x] `사용자에게 재입력 액션` + - [x] 로또 구입 금액에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [x] 당첨 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [x] 보너스 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. +- [x] `난수를 생성 액션` + - [x] 1 <= n <= 45 범위의 난수 n을 생성한다. - [ ] `로또 시뮬레이션 결과 출력 액션` - [ ] 발행한 로또의 수량과 번호을 출력한다. - [ ] 당첨 내역을 출력한다. diff --git a/__tests__/ProfitDomainUnit.test.js b/__tests__/ProfitDomainUnit.test.js index 2ab2e74d0..9fa3ae474 100644 --- a/__tests__/ProfitDomainUnit.test.js +++ b/__tests__/ProfitDomainUnit.test.js @@ -1,5 +1,5 @@ import { accumulateProfit, getRateOfInvestmentByPercent } from "../src/domains/profit"; -import { PRIZE_TABLE } from "../src/constants"; +import { PRIZE_TABLE } from "../src/constants/lotto"; describe("로또 수익 관련 로직 단위 테스트", () => { test("1등, 4등을 했을 때 2,000,050,000을 반환한다.", () => { diff --git a/__tests__/RankingDomainUnit.test.js b/__tests__/RankingDomainUnit.test.js index 9de6319bb..c591509b1 100644 --- a/__tests__/RankingDomainUnit.test.js +++ b/__tests__/RankingDomainUnit.test.js @@ -1,5 +1,5 @@ import { calculateMatchCount, isBonusMatch, determineRankOf } from "../src/domains/ranking"; -import { RANK_TABLE } from "../src/constants"; +import { RANK_TABLE } from "../src/constants/lotto"; describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { test("일치하는 개수를 반환한다.", () => { diff --git a/__tests__/ValidateDomainUnit.test.js b/__tests__/ValidateDomainUnit.test.js index 61b89f38a..8c3cb5476 100644 --- a/__tests__/ValidateDomainUnit.test.js +++ b/__tests__/ValidateDomainUnit.test.js @@ -1,5 +1,5 @@ import { validateLottoNumbers, validateBonusNumber, validateCost } from "../src/domains/validate"; -import { ERROR_MSG } from "../src/constants"; +import { ERROR_MSG } from "../src/constants/lotto"; describe("당첨 번호 도메인 검증", () => { test("로또 번호는 6개이어야 한다.", () => { diff --git a/src/App.js b/src/App.js index cc7568d93..6870723c9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,32 @@ -import { createLottos } from "./domains/createLottoNumbers"; -import Lotto from "./entities/Lotto"; - +import { INPUT_QUESTION } from "./constants/ioMsg"; +import { askUntilValid } from "./askUntilValid"; +import { parseToArrayByComma, toNumber } from "./utils/parsing"; +import { validateBonusNumber, validateCost, validateLottoNumbers } from "./domains/validate"; class App { - async run() {} - generateLottoObjects(quantity) { - const lottos = createLottos(); - new Lotto + async run() { + const tickets = await askUntilValid({ + question: INPUT_QUESTION.COST, + parse: (raw) => { + const n = toNumber(raw); + if(!Number.isInteger(n)) throw new Error("[ERROR]"); + return n; + }, + makeAndValidate: validateCost + }); + const winningNums = await askUntilValid({ + question: INPUT_QUESTION.WINNING_NUMS, + parse: parseToArrayByComma, + makeAndValidate: validateLottoNumbers, + }); + const bonusNum = await askUntilValid({ + question: INPUT_QUESTION.COST, + parse: (raw) => { + const n = toNumber(raw); + if(!Number.isInteger(n)) throw new Error("[ERROR]"); + return n; + }, + makeAndValidate: (n) => validateBonusNumber(n, winningNums), + }); } } diff --git a/src/askUntilValid.js b/src/askUntilValid.js new file mode 100644 index 000000000..703bc322d --- /dev/null +++ b/src/askUntilValid.js @@ -0,0 +1,14 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +export async function askUntilValid({ question, parse, makeAndValidate }) { + while (true) { + try { + const raw = (await MissionUtils.Console.readLineAsync(question)).trim(); + const parsed = parse(raw); + const value = makeAndValidate(parsed); + if(value !== true) throw new Error(value); + return parsed; + } catch (e) { + MissionUtils.Console.print(e.message); // "[ERROR] ..." 출력 후 루프 지속 + } + } +} diff --git a/src/constants/ioMsg.js b/src/constants/ioMsg.js new file mode 100644 index 000000000..3515ed125 --- /dev/null +++ b/src/constants/ioMsg.js @@ -0,0 +1,5 @@ +export const INPUT_QUESTION = Object.freeze({ + COST: "구입금액을 입력해 주세요.", + WINNING_NUMS: "당첨 번호를 입력해 주세요.", + BONUS_NUM: "보너스 번호를 입력해 주세요.", +}) \ No newline at end of file diff --git a/src/constants.js b/src/constants/lotto.js similarity index 100% rename from src/constants.js rename to src/constants/lotto.js diff --git a/src/domains/createLottoNumbers.js b/src/domains/createLottoNumbers.js index 81704f7f3..2f780749a 100644 --- a/src/domains/createLottoNumbers.js +++ b/src/domains/createLottoNumbers.js @@ -1,4 +1,4 @@ -import { LOTTO_CONSTANTS } from "../constants" +import { LOTTO_CONSTANTS } from "../constants/lotto" import Lotto from "../entities/Lotto"; export function creatOneLotto(drawUniqueNumbers) { diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js index 7bd19db9d..0f9c79184 100644 --- a/src/domains/lottoRules.js +++ b/src/domains/lottoRules.js @@ -1,4 +1,4 @@ -import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants" +import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants/lotto" import { toArray, includesNumber } from "../utils/array"; // 로또의 비지니스 규칙과 관련된 검증들 From 219fd71a5116230d067344419e215b46effc7fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 21:54:01 +0900 Subject: [PATCH 22/29] feat(domain): implement validate isInteger --- README.md | 9 +++++---- __tests__/ValidateDomainUnit.test.js | 4 ++++ src/App.js | 12 ++---------- src/constants/lotto.js | 9 +++++---- src/domains/lottoRules.js | 15 +++++++++++---- src/domains/ranking.js | 2 +- src/utils/{array.js => index.js} | 5 ++++- 7 files changed, 32 insertions(+), 24 deletions(-) rename src/utils/{array.js => index.js} (61%) diff --git a/README.md b/README.md index 61f55f570..aa3281d02 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - [x] `사용자의 입력값 검증 계산` - [x] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. - [x] 로또 당첨 번호가 6개인지 검증한다. + - [x] 번호가 정수인지 검증한다. - [x] 로또 당첨 번호가 중복되지 않는 지 검증한다. - [x] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. - [x] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. @@ -27,10 +28,10 @@ - [x] 퍼센트 수익률을 계산한다. ### Effect(부수효과 액션: 입출력/난수/에러) -- [ ] `사용자의 입력 액션` - - [ ] 로또 구입 금액을 읽는다. - - [ ] 로또 당첨 번호를 읽는다. - - [ ] 보너스 번호를 읽는다. +- [x] `사용자의 입력 액션` + - [x] 로또 구입 금액을 읽는다. + - [x] 로또 당첨 번호를 읽는다. + - [x] 보너스 번호를 읽는다. - [ ] `에러 발생 액션` - [ ] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. - [x] `사용자에게 재입력 액션` diff --git a/__tests__/ValidateDomainUnit.test.js b/__tests__/ValidateDomainUnit.test.js index 8c3cb5476..cd1958f0e 100644 --- a/__tests__/ValidateDomainUnit.test.js +++ b/__tests__/ValidateDomainUnit.test.js @@ -5,6 +5,10 @@ describe("당첨 번호 도메인 검증", () => { test("로또 번호는 6개이어야 한다.", () => { expect(validateLottoNumbers([1, 2, 3, 4, 5])).toBe(ERROR_MSG.LOTTO_SIZE); }); + test("로또 번호는 정수이어야 한다.", () => { + expect(validateLottoNumbers(["m", 1, 2, 3, 4, 5])).toBe(ERROR_MSG.NUMBER_INTEGER); + expect(validateLottoNumbers([1.1, 1, 2, 3, 4, 5])).toBe(ERROR_MSG.NUMBER_INTEGER); + }) test("각 로또 번호는 1~45 범위 안에 있어야 한다.", () => { expect(validateLottoNumbers([0, 1, 2, 3, 4, 5])).toBe(ERROR_MSG.LOTTO_NUM_RANGE); // < 1 expect(validateLottoNumbers([1, 2, 3, 4, 5, 46])).toBe(ERROR_MSG.LOTTO_NUM_RANGE); // > 45 diff --git a/src/App.js b/src/App.js index 6870723c9..08feda29d 100644 --- a/src/App.js +++ b/src/App.js @@ -6,11 +6,7 @@ class App { async run() { const tickets = await askUntilValid({ question: INPUT_QUESTION.COST, - parse: (raw) => { - const n = toNumber(raw); - if(!Number.isInteger(n)) throw new Error("[ERROR]"); - return n; - }, + parse: toNumber, makeAndValidate: validateCost }); const winningNums = await askUntilValid({ @@ -20,11 +16,7 @@ class App { }); const bonusNum = await askUntilValid({ question: INPUT_QUESTION.COST, - parse: (raw) => { - const n = toNumber(raw); - if(!Number.isInteger(n)) throw new Error("[ERROR]"); - return n; - }, + parse: toNumber, makeAndValidate: (n) => validateBonusNumber(n, winningNums), }); } diff --git a/src/constants/lotto.js b/src/constants/lotto.js index 179ea55ea..53658c41f 100644 --- a/src/constants/lotto.js +++ b/src/constants/lotto.js @@ -25,8 +25,9 @@ export const PRIZE_TABLE = Object.freeze({ }); export const ERROR_MSG = Object.freeze({ - COST_UNIT: "[ERROR] 구입 금액은 1000원 단위의 숫자여야 합니다.", - LOTTO_SIZE: "[ERROR] 로또 번호는 6개여야 합니다.", - LOTTO_NUM_RANGE: "[ERROR] 로또 번호는 1~45만으로 이루어집니다.", - LOTTO_NUM_UNIQUE: "[ERROR] 하나의 로또에 중복된 숫자가 존재할 수 없습니다.", + COST_UNIT: "[ERROR] 구입 금액은 1000원 단위의 숫자여야 합니다.\n", + LOTTO_SIZE: "[ERROR] 로또 번호는 6개여야 합니다.\n", + LOTTO_NUM_RANGE: "[ERROR] 로또 번호는 1~45만으로 이루어집니다.\n", + LOTTO_NUM_UNIQUE: "[ERROR] 하나의 로또에 중복된 숫자가 존재할 수 없습니다.\n", + NUMBER_INTEGER: "[ERROR] 로또 번호는 정수입니다.\n" }) \ No newline at end of file diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js index 0f9c79184..0a0668e20 100644 --- a/src/domains/lottoRules.js +++ b/src/domains/lottoRules.js @@ -1,5 +1,5 @@ import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants/lotto" -import { toArray, includesNumber } from "../utils/array"; +import { toArray, includesNumber, isIntegerValue } from "../utils"; // 로또의 비지니스 규칙과 관련된 검증들 export const isValidPurchaseAmount = (amount) => @@ -17,13 +17,20 @@ export const inRange = (valueOrArr) => { ) || ERROR_MSG.LOTTO_NUM_RANGE; } +export const isIntegerArr = (valueOrArr) => { + const arr = toArray(valueOrArr); + return arr.every( + n => isIntegerValue(n) + ) || ERROR_MSG.NUMBER_INTEGER; +} + export const isLottoNumUnique = (arr) => new Set(arr).size === arr.length || ERROR_MSG.LOTTO_NUM_UNIQUE; export const isBonusUnique = (num, arr) => !includesNumber(arr, num) || ERROR_MSG.LOTTO_NUM_UNIQUE; -export const costRules = [isValidPurchaseAmount]; -export const lottoRules = [hasExactSize, inRange, isLottoNumUnique]; -export const bonusRules = [inRange, isBonusUnique]; +export const costRules = [isIntegerArr, isValidPurchaseAmount]; +export const lottoRules = [hasExactSize, isIntegerArr, inRange, isLottoNumUnique]; +export const bonusRules = [isIntegerArr, inRange, isBonusUnique]; diff --git a/src/domains/ranking.js b/src/domains/ranking.js index 75d86b763..5ddf32a08 100644 --- a/src/domains/ranking.js +++ b/src/domains/ranking.js @@ -1,4 +1,4 @@ -import { includesNumber } from "../utils/array"; +import { includesNumber } from "../utils"; export function calculateMatchCount(ticket, winning, numbersPerTicket = 6) { const uniqueNumbers = new Set([...ticket, ...winning]); diff --git a/src/utils/array.js b/src/utils/index.js similarity index 61% rename from src/utils/array.js rename to src/utils/index.js index 6e96ee9eb..772e67318 100644 --- a/src/utils/array.js +++ b/src/utils/index.js @@ -4,4 +4,7 @@ export const includesNumber = (arr, num) => arr.includes(num); export const toArray = (value) => { if(Array.isArray(value)) return value; return [value]; -} \ No newline at end of file +} + +export const isIntegerValue = (value) => Number.isInteger(value); +export const isNaNValue = (value) => Number.isNaN(value); \ No newline at end of file From 04163be5f2935da45a6685cfb65473626a515eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 22:11:08 +0900 Subject: [PATCH 23/29] feat(app): print purchased amount and generated numbers --- README.md | 2 +- src/App.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aa3281d02..53dbc158d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,6 @@ - [x] `난수를 생성 액션` - [x] 1 <= n <= 45 범위의 난수 n을 생성한다. - [ ] `로또 시뮬레이션 결과 출력 액션` - - [ ] 발행한 로또의 수량과 번호을 출력한다. + - [x] 발행한 로또의 수량과 번호을 출력한다. - [ ] 당첨 내역을 출력한다. - [ ] 수익률을 출력한다. \ No newline at end of file diff --git a/src/App.js b/src/App.js index 08feda29d..f11d801bc 100644 --- a/src/App.js +++ b/src/App.js @@ -1,14 +1,27 @@ import { INPUT_QUESTION } from "./constants/ioMsg"; +import { LOTTO_CONSTANTS } from "./constants/lotto"; import { askUntilValid } from "./askUntilValid"; -import { parseToArrayByComma, toNumber } from "./utils/parsing"; +import { devisionNumber, parseToArrayByComma, toNumber } from "./utils/parsing"; import { validateBonusNumber, validateCost, validateLottoNumbers } from "./domains/validate"; +import { createLottos } from "./domains/createLottoNumbers"; +import { MissionUtils } from "@woowacourse/mission-utils"; +import { randomUniquesInRange } from "./utils/random"; + class App { async run() { - const tickets = await askUntilValid({ + const purchasedAmount = await askUntilValid({ question: INPUT_QUESTION.COST, parse: toNumber, makeAndValidate: validateCost }); + const ticketAmount = devisionNumber(purchasedAmount, LOTTO_CONSTANTS.TICKET_PRICE); + const lottos = createLottos(ticketAmount, randomUniquesInRange); + + MissionUtils.Console.print(`${ticketAmount}개를 구매했습니다.`); + lottos.forEach((lotto) => { + MissionUtils.Console.print(`[${lotto.numbers.join(", ")}]`); + }); + const winningNums = await askUntilValid({ question: INPUT_QUESTION.WINNING_NUMS, parse: parseToArrayByComma, From 0295c78f58f94dace4a0e60df939db3a7db3aabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 22:27:02 +0900 Subject: [PATCH 24/29] feat(app): print lotto result statistics --- README.md | 4 ++-- src/App.js | 26 ++++++++++++++++++++++++-- src/constants/ioMsg.js | 10 +++++++++- src/domains/ranking.js | 2 +- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 53dbc158d..4ca1e1bf6 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ - [x] 로또 구입 금액을 읽는다. - [x] 로또 당첨 번호를 읽는다. - [x] 보너스 번호를 읽는다. -- [ ] `에러 발생 액션` - - [ ] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. +- [x] `에러 발생 액션` + - [x] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. - [x] `사용자에게 재입력 액션` - [x] 로또 구입 금액에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. - [x] 당첨 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. diff --git a/src/App.js b/src/App.js index f11d801bc..576494c1c 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,12 @@ -import { INPUT_QUESTION } from "./constants/ioMsg"; -import { LOTTO_CONSTANTS } from "./constants/lotto"; +import { INPUT_QUESTION, LABELS } from "./constants/ioMsg"; +import { LOTTO_CONSTANTS, PRIZE_TABLE, RANK_TABLE } from "./constants/lotto"; import { askUntilValid } from "./askUntilValid"; import { devisionNumber, parseToArrayByComma, toNumber } from "./utils/parsing"; import { validateBonusNumber, validateCost, validateLottoNumbers } from "./domains/validate"; import { createLottos } from "./domains/createLottoNumbers"; import { MissionUtils } from "@woowacourse/mission-utils"; import { randomUniquesInRange } from "./utils/random"; +import { calculateMatchCount, determineRankOf, isBonusMatch } from "./domains/ranking"; class App { async run() { @@ -27,6 +28,27 @@ class App { parse: parseToArrayByComma, makeAndValidate: validateLottoNumbers, }); + + const results = lottos.map((lotto) => { + const matched = calculateMatchCount(lotto.numbers, winningNums); + const bonusFlag = isBonusMatch(lotto.numbers, bonusNum); + const rank = determineRankOf(matched, bonusFlag, RANK_TABLE); + return { matched, bonusFlag, rank }; + }); + + const rankCounts = results.reduce((acc, { rank }) => { + if (rank) acc[rank] = (acc[rank] || 0) + 1; + return acc; + }, {}); + + MissionUtils.Console.print("\n당첨 통계"); + MissionUtils.Console.print("---"); + Object.entries(PRIZE_TABLE).forEach(([rank, prize]) => { + const count = rankCounts[rank] || 0; + MissionUtils.Console.print(`${LABELS[rank]} (${prize.toLocaleString()}원) - ${count}개`); + }); + + const bonusNum = await askUntilValid({ question: INPUT_QUESTION.COST, parse: toNumber, diff --git a/src/constants/ioMsg.js b/src/constants/ioMsg.js index 3515ed125..d6026db27 100644 --- a/src/constants/ioMsg.js +++ b/src/constants/ioMsg.js @@ -2,4 +2,12 @@ export const INPUT_QUESTION = Object.freeze({ COST: "구입금액을 입력해 주세요.", WINNING_NUMS: "당첨 번호를 입력해 주세요.", BONUS_NUM: "보너스 번호를 입력해 주세요.", -}) \ No newline at end of file +}) + +export const LABELS = { + 5: "3개 일치", + 4: "4개 일치", + 3: "5개 일치", + 2: "5개 일치, 보너스 볼 일치", + 1: "6개 일치", +}; diff --git a/src/domains/ranking.js b/src/domains/ranking.js index 5ddf32a08..444588829 100644 --- a/src/domains/ranking.js +++ b/src/domains/ranking.js @@ -12,4 +12,4 @@ export function isBonusMatch(ticket, bonusNum) { export function determineRankOf(matchedCnt, bonusFlag, RANK_TABLE) { const rank = RANK_TABLE[matchedCnt][bonusFlag]; return rank; -}; \ No newline at end of file +}; From d2d5566eef0810ca0a1270f8137db4d570b5f4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 22:45:42 +0900 Subject: [PATCH 25/29] feat(app): print rate of investment --- src/App.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/App.js b/src/App.js index 576494c1c..2e699afae 100644 --- a/src/App.js +++ b/src/App.js @@ -7,6 +7,7 @@ import { createLottos } from "./domains/createLottoNumbers"; import { MissionUtils } from "@woowacourse/mission-utils"; import { randomUniquesInRange } from "./utils/random"; import { calculateMatchCount, determineRankOf, isBonusMatch } from "./domains/ranking"; +import { accumulateProfit, getRateOfInvestmentByPercent } from "./domains/profit"; class App { async run() { @@ -29,12 +30,22 @@ class App { makeAndValidate: validateLottoNumbers, }); + const bonusNum = await askUntilValid({ + question: INPUT_QUESTION.COST, + parse: toNumber, + makeAndValidate: (n) => validateBonusNumber(n, winningNums), + }); + const results = lottos.map((lotto) => { const matched = calculateMatchCount(lotto.numbers, winningNums); const bonusFlag = isBonusMatch(lotto.numbers, bonusNum); const rank = determineRankOf(matched, bonusFlag, RANK_TABLE); return { matched, bonusFlag, rank }; }); + + const rankResultArr = results.map(({rank}) => rank); + const totalProfit = accumulateProfit(rankResultArr, PRIZE_TABLE); + const rateOfInvestment = getRateOfInvestmentByPercent(totalProfit, purchasedAmount); const rankCounts = results.reduce((acc, { rank }) => { if (rank) acc[rank] = (acc[rank] || 0) + 1; @@ -47,13 +58,7 @@ class App { const count = rankCounts[rank] || 0; MissionUtils.Console.print(`${LABELS[rank]} (${prize.toLocaleString()}원) - ${count}개`); }); - - - const bonusNum = await askUntilValid({ - question: INPUT_QUESTION.COST, - parse: toNumber, - makeAndValidate: (n) => validateBonusNumber(n, winningNums), - }); + MissionUtils.Console.print(`총 수익률은 ${rateOfInvestment}%입니다.`) } } From e7df964b551f79f023ea361c4c5dba27ebcfd72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 22:52:37 +0900 Subject: [PATCH 26/29] refactor(app): make output massages to constant object --- src/App.js | 10 +++++----- src/constants/ioMsg.js | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/App.js b/src/App.js index 2e699afae..a80face60 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import { INPUT_QUESTION, LABELS } from "./constants/ioMsg"; +import { INPUT_QUESTION, LABELS, OUTPUT_MSG } from "./constants/ioMsg"; import { LOTTO_CONSTANTS, PRIZE_TABLE, RANK_TABLE } from "./constants/lotto"; import { askUntilValid } from "./askUntilValid"; import { devisionNumber, parseToArrayByComma, toNumber } from "./utils/parsing"; @@ -19,7 +19,7 @@ class App { const ticketAmount = devisionNumber(purchasedAmount, LOTTO_CONSTANTS.TICKET_PRICE); const lottos = createLottos(ticketAmount, randomUniquesInRange); - MissionUtils.Console.print(`${ticketAmount}개를 구매했습니다.`); + MissionUtils.Console.print(OUTPUT_MSG.PURCHASED_TICKETS(ticketAmount)); lottos.forEach((lotto) => { MissionUtils.Console.print(`[${lotto.numbers.join(", ")}]`); }); @@ -52,13 +52,13 @@ class App { return acc; }, {}); - MissionUtils.Console.print("\n당첨 통계"); - MissionUtils.Console.print("---"); + MissionUtils.Console.print(OUTPUT_MSG.WINNING_STATS_DIVIDER); + MissionUtils.Console.print(OUTPUT_MSG.WINNING_STATS_DIVIDER); Object.entries(PRIZE_TABLE).forEach(([rank, prize]) => { const count = rankCounts[rank] || 0; MissionUtils.Console.print(`${LABELS[rank]} (${prize.toLocaleString()}원) - ${count}개`); }); - MissionUtils.Console.print(`총 수익률은 ${rateOfInvestment}%입니다.`) + MissionUtils.Console.print(OUTPUT_MSG.ROI_RESULT(rateOfInvestment)); } } diff --git a/src/constants/ioMsg.js b/src/constants/ioMsg.js index d6026db27..0c635f1c9 100644 --- a/src/constants/ioMsg.js +++ b/src/constants/ioMsg.js @@ -2,7 +2,14 @@ export const INPUT_QUESTION = Object.freeze({ COST: "구입금액을 입력해 주세요.", WINNING_NUMS: "당첨 번호를 입력해 주세요.", BONUS_NUM: "보너스 번호를 입력해 주세요.", -}) +}); + +export const OUTPUT_MSG = Object.freeze({ + PURCHASED_TICKETS: (count) => `${count}개를 구매했습니다.`, + WINNING_STATS_HEADER: "\n당첨 통계", + WINNING_STATS_DIVIDER: "---", + ROI_RESULT: (roi) => `총 수익률은 ${roi}%입니다.`, +}); export const LABELS = { 5: "3개 일치", From df46dd1db6161b6ad05cdf795b164cb702863707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 23:08:34 +0900 Subject: [PATCH 27/29] refactor(view): seperate input function to view --- src/App.js | 26 ++++++-------------------- src/view/input.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 src/view/input.js diff --git a/src/App.js b/src/App.js index a80face60..c3c7dd36b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,7 @@ -import { INPUT_QUESTION, LABELS, OUTPUT_MSG } from "./constants/ioMsg"; +import { LABELS, OUTPUT_MSG } from "./constants/ioMsg"; import { LOTTO_CONSTANTS, PRIZE_TABLE, RANK_TABLE } from "./constants/lotto"; -import { askUntilValid } from "./askUntilValid"; -import { devisionNumber, parseToArrayByComma, toNumber } from "./utils/parsing"; -import { validateBonusNumber, validateCost, validateLottoNumbers } from "./domains/validate"; +import { readBonusNumberUntilValid, readPurchasedAmountUntilValid, readWinningNumbersUntilValid } from "./view/input"; +import { devisionNumber } from "./utils/parsing"; import { createLottos } from "./domains/createLottoNumbers"; import { MissionUtils } from "@woowacourse/mission-utils"; import { randomUniquesInRange } from "./utils/random"; @@ -11,11 +10,7 @@ import { accumulateProfit, getRateOfInvestmentByPercent } from "./domains/profit class App { async run() { - const purchasedAmount = await askUntilValid({ - question: INPUT_QUESTION.COST, - parse: toNumber, - makeAndValidate: validateCost - }); + const purchasedAmount = await readPurchasedAmountUntilValid(); const ticketAmount = devisionNumber(purchasedAmount, LOTTO_CONSTANTS.TICKET_PRICE); const lottos = createLottos(ticketAmount, randomUniquesInRange); @@ -24,17 +19,8 @@ class App { MissionUtils.Console.print(`[${lotto.numbers.join(", ")}]`); }); - const winningNums = await askUntilValid({ - question: INPUT_QUESTION.WINNING_NUMS, - parse: parseToArrayByComma, - makeAndValidate: validateLottoNumbers, - }); - - const bonusNum = await askUntilValid({ - question: INPUT_QUESTION.COST, - parse: toNumber, - makeAndValidate: (n) => validateBonusNumber(n, winningNums), - }); + const winningNums = await readWinningNumbersUntilValid(); + const bonusNum = await readBonusNumberUntilValid(winningNums); const results = lottos.map((lotto) => { const matched = calculateMatchCount(lotto.numbers, winningNums); diff --git a/src/view/input.js b/src/view/input.js new file mode 100644 index 000000000..d42f3025f --- /dev/null +++ b/src/view/input.js @@ -0,0 +1,28 @@ +import { askUntilValid } from "../askUntilValid"; +import { INPUT_QUESTION } from "../constants/ioMsg"; +import { parseToArrayByComma, toNumber } from "../utils/parsing"; +import { validateBonusNumber, validateCost, validateLottoNumbers } from "../domains/validate"; + +export async function readPurchasedAmountUntilValid() { + return askUntilValid({ + question: INPUT_QUESTION.COST, + parse: toNumber, + makeAndValidate: validateCost, + }); +} + +export async function readWinningNumbersUntilValid() { + return askUntilValid({ + question: INPUT_QUESTION.WINNING_NUMS, + parse: parseToArrayByComma, + makeAndValidate: validateLottoNumbers, + }); +} + +export async function readBonusNumberUntilValid(winning) { + return askUntilValid({ + question: INPUT_QUESTION.BONUS_NUM, + parse: toNumber, + makeAndValidate: (n) => validateBonusNumber(n, winning), + }); +} \ No newline at end of file From 15fbced1b6758ed468cbfcaef227212b8da7fbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 23:32:00 +0900 Subject: [PATCH 28/29] refactor(view): make print view and seperate app.run() --- src/App.js | 29 ++++++++--------------------- src/domains/ranking.js | 10 ++++++++++ src/{ => view}/askUntilValid.js | 4 ++-- src/view/input.js | 8 ++++---- src/view/output.js | 24 ++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 27 deletions(-) rename src/{ => view}/askUntilValid.js (76%) create mode 100644 src/view/output.js diff --git a/src/App.js b/src/App.js index c3c7dd36b..a5573d474 100644 --- a/src/App.js +++ b/src/App.js @@ -5,46 +5,33 @@ import { devisionNumber } from "./utils/parsing"; import { createLottos } from "./domains/createLottoNumbers"; import { MissionUtils } from "@woowacourse/mission-utils"; import { randomUniquesInRange } from "./utils/random"; -import { calculateMatchCount, determineRankOf, isBonusMatch } from "./domains/ranking"; +import { calculateMatchCount, determineRankOf, getResultOfLotto, isBonusMatch } from "./domains/ranking"; import { accumulateProfit, getRateOfInvestmentByPercent } from "./domains/profit"; +import { printGeneratedLottos, printResultStats } from "./view/output"; class App { async run() { const purchasedAmount = await readPurchasedAmountUntilValid(); const ticketAmount = devisionNumber(purchasedAmount, LOTTO_CONSTANTS.TICKET_PRICE); - const lottos = createLottos(ticketAmount, randomUniquesInRange); - MissionUtils.Console.print(OUTPUT_MSG.PURCHASED_TICKETS(ticketAmount)); - lottos.forEach((lotto) => { - MissionUtils.Console.print(`[${lotto.numbers.join(", ")}]`); - }); + const lottos = createLottos(ticketAmount, randomUniquesInRange); // 난수 생성 함수를 주입 + + printGeneratedLottos(ticketAmount, lottos); const winningNums = await readWinningNumbersUntilValid(); const bonusNum = await readBonusNumberUntilValid(winningNums); - const results = lottos.map((lotto) => { - const matched = calculateMatchCount(lotto.numbers, winningNums); - const bonusFlag = isBonusMatch(lotto.numbers, bonusNum); - const rank = determineRankOf(matched, bonusFlag, RANK_TABLE); - return { matched, bonusFlag, rank }; - }); - + const results = getResultOfLotto(lottos, winningNums, bonusNum); const rankResultArr = results.map(({rank}) => rank); const totalProfit = accumulateProfit(rankResultArr, PRIZE_TABLE); - const rateOfInvestment = getRateOfInvestmentByPercent(totalProfit, purchasedAmount); + const rateOfInvestment = getRateOfInvestmentByPercent(totalProfit, purchasedAmount); // const rankCounts = results.reduce((acc, { rank }) => { if (rank) acc[rank] = (acc[rank] || 0) + 1; return acc; }, {}); - MissionUtils.Console.print(OUTPUT_MSG.WINNING_STATS_DIVIDER); - MissionUtils.Console.print(OUTPUT_MSG.WINNING_STATS_DIVIDER); - Object.entries(PRIZE_TABLE).forEach(([rank, prize]) => { - const count = rankCounts[rank] || 0; - MissionUtils.Console.print(`${LABELS[rank]} (${prize.toLocaleString()}원) - ${count}개`); - }); - MissionUtils.Console.print(OUTPUT_MSG.ROI_RESULT(rateOfInvestment)); + printResultStats(rankCounts, rateOfInvestment); } } diff --git a/src/domains/ranking.js b/src/domains/ranking.js index 444588829..2c23cc41e 100644 --- a/src/domains/ranking.js +++ b/src/domains/ranking.js @@ -1,4 +1,5 @@ import { includesNumber } from "../utils"; +import { RANK_TABLE } from "../constants/lotto"; export function calculateMatchCount(ticket, winning, numbersPerTicket = 6) { const uniqueNumbers = new Set([...ticket, ...winning]); @@ -13,3 +14,12 @@ export function determineRankOf(matchedCnt, bonusFlag, RANK_TABLE) { const rank = RANK_TABLE[matchedCnt][bonusFlag]; return rank; }; + +export function getResultOfLotto(lottos, winningNums, bonusNum) { + return lottos.map((lotto) => { + const matched = calculateMatchCount(lotto.numbers, winningNums); + const bonusFlag = isBonusMatch(lotto.numbers, bonusNum); + const rank = determineRankOf(matched, bonusFlag, RANK_TABLE); + return { matched, bonusFlag, rank }; + }); +}; diff --git a/src/askUntilValid.js b/src/view/askUntilValid.js similarity index 76% rename from src/askUntilValid.js rename to src/view/askUntilValid.js index 703bc322d..2e3139077 100644 --- a/src/askUntilValid.js +++ b/src/view/askUntilValid.js @@ -1,10 +1,10 @@ import { MissionUtils } from "@woowacourse/mission-utils"; -export async function askUntilValid({ question, parse, makeAndValidate }) { +export async function askUntilValid({ question, parse, validate }) { while (true) { try { const raw = (await MissionUtils.Console.readLineAsync(question)).trim(); const parsed = parse(raw); - const value = makeAndValidate(parsed); + const value = validate(parsed); if(value !== true) throw new Error(value); return parsed; } catch (e) { diff --git a/src/view/input.js b/src/view/input.js index d42f3025f..8d2e386a5 100644 --- a/src/view/input.js +++ b/src/view/input.js @@ -1,4 +1,4 @@ -import { askUntilValid } from "../askUntilValid"; +import { askUntilValid } from "./askUntilValid"; import { INPUT_QUESTION } from "../constants/ioMsg"; import { parseToArrayByComma, toNumber } from "../utils/parsing"; import { validateBonusNumber, validateCost, validateLottoNumbers } from "../domains/validate"; @@ -7,7 +7,7 @@ export async function readPurchasedAmountUntilValid() { return askUntilValid({ question: INPUT_QUESTION.COST, parse: toNumber, - makeAndValidate: validateCost, + validate: validateCost, }); } @@ -15,7 +15,7 @@ export async function readWinningNumbersUntilValid() { return askUntilValid({ question: INPUT_QUESTION.WINNING_NUMS, parse: parseToArrayByComma, - makeAndValidate: validateLottoNumbers, + validate: validateLottoNumbers, }); } @@ -23,6 +23,6 @@ export async function readBonusNumberUntilValid(winning) { return askUntilValid({ question: INPUT_QUESTION.BONUS_NUM, parse: toNumber, - makeAndValidate: (n) => validateBonusNumber(n, winning), + validate: (n) => validateBonusNumber(n, winning), }); } \ No newline at end of file diff --git a/src/view/output.js b/src/view/output.js new file mode 100644 index 000000000..7faf51d31 --- /dev/null +++ b/src/view/output.js @@ -0,0 +1,24 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import { OUTPUT_MSG, LABELS } from "../constants/ioMsg"; +import { PRIZE_TABLE } from "../constants/lotto"; + +export function printGeneratedLottos(ticketAmount, lottos) { + printUsingWoowa(OUTPUT_MSG.PURCHASED_TICKETS(ticketAmount)) + lottos.forEach((lotto) => { + printUsingWoowa(`[${lotto.numbers.join(", ")}]`); + }); +} + +export function printResultStats(rankCounts, roi) { + printUsingWoowa(OUTPUT_MSG.WINNING_STATS_HEADER); + printUsingWoowa(OUTPUT_MSG.WINNING_STATS_DIVIDER); + Object.entries(PRIZE_TABLE).forEach(([rank, prize]) => { + const count = rankCounts[rank] || 0; + printUsingWoowa(`${LABELS[rank]} (${prize.toLocaleString()}원) - ${count}개`); + }); + printUsingWoowa(OUTPUT_MSG.ROI_RESULT(roi)); +} + +function printUsingWoowa(msg) { + MissionUtils.Console.print(msg); +} \ No newline at end of file From a1cb95beba26b8a42115c77ca41a153bae0ed9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 3 Nov 2025 23:36:16 +0900 Subject: [PATCH 29/29] docs(readme): check feature list --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4ca1e1bf6..f298a3d09 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ - [x] 보너스 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. - [x] `난수를 생성 액션` - [x] 1 <= n <= 45 범위의 난수 n을 생성한다. -- [ ] `로또 시뮬레이션 결과 출력 액션` +- [x] `로또 시뮬레이션 결과 출력 액션` - [x] 발행한 로또의 수량과 번호을 출력한다. - - [ ] 당첨 내역을 출력한다. - - [ ] 수익률을 출력한다. \ No newline at end of file + - [x] 당첨 내역을 출력한다. + - [x] 수익률을 출력한다. \ No newline at end of file