From 96ca9c99ba56bcdf6212c44b2401ae60b8f7a80f Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:03:01 +0900 Subject: [PATCH 01/20] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=ED=95=A0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=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 | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index 15bb106b5..fe4dbdb85 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ # javascript-lotto-precourse +# 3주차 - 로또 + +## ⚙️ 간단한 프로젝트 실행 흐름 +1. 안내 문구를 출력한다. +2. 로또 구입 금액을 입력받아 검증한다.(1,000원 단위) +3. 구입 장수만큼 로또를 발행하고 오름차순으로 출력한다. +4. 당첨 번호(6개, 쉼표 구분)를 입력받아 검증한다. +5. 보너스 번호(1개)를 입력받아 검증한다. +6. 구매 로또 vs 담청 번호를 비교해 등수별 개수를 계산한다. +7. 총 수익률(소수점 둘째 자리 반올림)을 출력한다. +8. 게임을 종료한다. +(사용자가 잘못된 값을 입력할 경우 "[ERROR]로 시작하는 메시지와 함께 Error 발생 + 해당 메시지 출력 후 다음 해당 지점부터 다시 입력을 받는다.) + + +## ⚠️ 고려사항 +- 로또 번호의 숫자 범위는 1~45이다. +- 로또 번호 6개와 보너스 번호 1개는 중복되지 않는다. +- 로또 구입 금액 입력 시, 그 금액에 해당하는 만큼 로또를 발행해야 한다. +- 로또 1장 가격 : 1,000원 + + +## 🔧 구현할 기능 목록 +### 1. util +- parser(문자열 -> 자료형 변환) + - [ ] parseAmount : 구입 금액 문자열 -> 정수 + - [ ] parseWinningNumbers : 로또 담청 번호 문자열 -> 정수 배열 + - [ ] parseBonusNumber : 보너스 번호 문자열 -> 정수 + +- validator(입력 규칙 검증) + - [ ] validateAmount : 정수 여부, 1000원 단위, 최소 1000원 + - [ ] validateTicketNumbers : 길이 6, 1~45, 중복 없음 + - [ ] validateWinningNumbers : 길이 6, 1~45, 중복 없음 + - [ ] validateBonusNumber : 1~45, 당첨 번호와 중복 불가 + +- format(출력 포맷) + - [ ] formatTicket : `[1, 2, 5, 14, 22, 45]` 문자열화 + - [ ] formatRate : 소수점 둘째 자리 반올림 퍼센트 `"62.5%"` + + +### 2. domain +- Lotto 클래스 : 생성자에서 번호 검증 +- Ranks, Prizes 상수 : 등수 규칙 및 상금 테이블 + + +### 3. service +- LottoService + - [ ] issueTickets : 금액 -> 정수 계산, Random Api 통한 티켓 발행 + - [ ] evaluate : 로또 번호 일치 개수 계산, 등수 판정, 집계 및 수익률 반환 +- matchCounter : 교집합 개수 판정 +- rankResolver : 보너스 포함 판정 + + +### 4. io +- [ ] InputView +- [ ] OutputView +- [ ] 공통 에러 처리 + + +### 5. App.js +- [ ] 모든 모듈 연동 +- [ ] `App.run()` 실행 흐름 완성 + + +### 6. test +- [ ] LottoTest.js : 테스트 코드 추가 및 테스트 +- [ ] ApplicationTest.js : 테스트 코드 실행 \ No newline at end of file From 5397573ecd251cd730b0db265c54182a8e07d0ca Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:32:11 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20parse=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/parser.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/util/parser.js diff --git a/src/util/parser.js b/src/util/parser.js new file mode 100644 index 000000000..8e1898397 --- /dev/null +++ b/src/util/parser.js @@ -0,0 +1,23 @@ +export function parseAmount(amountStr) { + const amount = String(amountStr ?? "").trim(); + if (!/^\d+$/.test(amount)) { + throw new Error("[Error] 구입 금액은 숫자여야 합니다."); + } + return Number(amount); +} + +export function parseWinningNumbers(numbersStr) { + const numbers = String(numbersStr ?? "").trim().split(",").map(num => num.trim()); + if (numbers.length !== 6 || !numbers.every(num => /^\d+$/.test(num))) { + throw new Error("[Error] 당첨 번호는 쉼표로 구분된 6개의 숫자여야 합니다."); + } + return numbers.map(Number); +} + +export function parseBonusNumber(bonusStr) { + const bonus = String(bonusStr ?? "").trim(); + if (!/^\d+$/.test(bonus)) { + throw new Error("[Error] 보너스 번호는 숫자여야 합니다."); + } + return Number(bonus); +} From e13ad9b9678cf50b5982dbe9877d13e5e65083ec Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:27:06 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants.js | 4 ++++ src/util/validator.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/constants.js create mode 100644 src/util/validator.js diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 000000000..23440546b --- /dev/null +++ b/src/constants.js @@ -0,0 +1,4 @@ +export const LOTTO_MIN = 1; +export const LOTTO_MAX = 45; +export const LOTTO_SIZE = 6; +export const LOTTO_PRICE = 1000; \ No newline at end of file diff --git a/src/util/validator.js b/src/util/validator.js new file mode 100644 index 000000000..6e6663362 --- /dev/null +++ b/src/util/validator.js @@ -0,0 +1,39 @@ +import { LOTTO_MIN, LOTTO_MAX, LOTTO_SIZE, LOTTO_PRICE } from "../constants.js"; + +const inRange = (number) => Number.isInteger(number) && LOTTO_MIN <= number && number <= LOTTO_MAX; + +export const validateAmount = (amount) => { + if (!Number.isInteger(amount)) { + throw new Error("[Error] 구입 금액은 숫자여야 합니다."); + } + if (amount < LOTTO_PRICE || amount % LOTTO_PRICE !== 0) { + throw new Error("[Error] 구입 금액은 1000원 단위여야 합니다."); + } + return amount; +} + +export const validateWinningNumbers = (numbers) => { + if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { + throw new Error("[Error] 로또 번호는 6개의 숫자여야 합니다."); + } + if (!numbers.some(number => !inRange(number))) { + throw new Error(`[Error] 로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + } + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== numbers.length) { + throw new Error("[Error] 로또 번호는 중복될 수 없습니다."); + } + return numbers; +} + + +export const validateBonusNumber = (bonusNumber, winningNumbers) => { + if (!inRange(bonusNumber)) { + throw new Error(`[Error] 보너스 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + } + const winningNumbers = new Set(winningNumbers); + if (winningNumbers.includes(bonusNumber)) { + throw new Error("[Error] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + return bonusNumber; +} \ No newline at end of file From 9621a22fdb7babeec24f651c8ef95928168d7f7c Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:27:35 +0900 Subject: [PATCH 04/20] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=ED=95=A0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=9E=91=EC=97=85=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fe4dbdb85..1a4cf08c6 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,14 @@ ## 🔧 구현할 기능 목록 ### 1. util - parser(문자열 -> 자료형 변환) - - [ ] parseAmount : 구입 금액 문자열 -> 정수 - - [ ] parseWinningNumbers : 로또 담청 번호 문자열 -> 정수 배열 - - [ ] parseBonusNumber : 보너스 번호 문자열 -> 정수 + - [X] parseAmount : 구입 금액 문자열 -> 정수 + - [X] parseWinningNumbers : 로또 담청 번호 문자열 -> 정수 배열 + - [X] parseBonusNumber : 보너스 번호 문자열 -> 정수 - validator(입력 규칙 검증) - - [ ] validateAmount : 정수 여부, 1000원 단위, 최소 1000원 - - [ ] validateTicketNumbers : 길이 6, 1~45, 중복 없음 - - [ ] validateWinningNumbers : 길이 6, 1~45, 중복 없음 - - [ ] validateBonusNumber : 1~45, 당첨 번호와 중복 불가 + - [X] validateAmount : 정수 여부, 1000원 단위, 최소 1000원 + - [X] validateWinningNumbers : 길이 6, 1~45, 중복 없음 + - [X] validateBonusNumber : 1~45, 당첨 번호와 중복 불가 - format(출력 포맷) - [ ] formatTicket : `[1, 2, 5, 14, 22, 45]` 문자열화 From d03328f688659cef989736a12beb6740b7a2315b Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:33:19 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EC=B6=9C=EB=A0=A5=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/format.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/util/format.js diff --git a/src/util/format.js b/src/util/format.js new file mode 100644 index 000000000..c08d712c4 --- /dev/null +++ b/src/util/format.js @@ -0,0 +1,6 @@ +export const formatTicket = (numbers) => `[${numbers.join(", ")}]`; + +export const formatRate = (value) => { + const rounded = Math.round(value * 10) / 10; + return `${rounded}%`; +}; From 32a08f395c6c6e997856b3a1f5bef23b0aba9f7f Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:53:34 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B2=BD=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- src/Lotto.js | 18 ------------------ src/domain/Lotto.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 22 deletions(-) delete mode 100644 src/Lotto.js create mode 100644 src/domain/Lotto.js diff --git a/README.md b/README.md index 1a4cf08c6..9c81ce36a 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,13 @@ - [X] validateBonusNumber : 1~45, 당첨 번호와 중복 불가 - format(출력 포맷) - - [ ] formatTicket : `[1, 2, 5, 14, 22, 45]` 문자열화 - - [ ] formatRate : 소수점 둘째 자리 반올림 퍼센트 `"62.5%"` + - [X] formatTicket : `[1, 2, 5, 14, 22, 45]` 문자열화 + - [X] formatRate : 소수점 둘째 자리 반올림 퍼센트 `"62.5%"` ### 2. domain -- Lotto 클래스 : 생성자에서 번호 검증 -- Ranks, Prizes 상수 : 등수 규칙 및 상금 테이블 +- [X] Lotto 클래스 : 생성자에서 번호 검증 +- [ ] Ranks, Prizes 상수 : 등수 규칙 및 상금 테이블 ### 3. service 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/domain/Lotto.js b/src/domain/Lotto.js new file mode 100644 index 000000000..d0c9eae61 --- /dev/null +++ b/src/domain/Lotto.js @@ -0,0 +1,30 @@ +import { LOTTO_MIN, LOTTO_MAX, LOTTO_SIZE } from "../constants.js"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = [...numbers].sort((a, b) => a - b); + } + + #validate(numbers) { + if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { + throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + } + const isRange = (number) => Number.isInteger(number) && LOTTO_MIN <= number && number <= LOTTO_MAX; + if (numbers.some(number => !isRange(number))) { + throw new Error(`[ERROR] 로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + } + const set = new Set(numbers); + if (set.size !== numbers.length) { + throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + } + + getNumbers() { + return [...this.#numbers]; + } +} + +export default Lotto; From 394c3b1ed3af0118767645870d0bb9a89f035abd Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:03:39 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20Rank,=20Prize=20=EC=83=81?= =?UTF-8?q?=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 | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/constants.js b/src/constants.js index 23440546b..10bbe877d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,22 @@ export const LOTTO_MIN = 1; export const LOTTO_MAX = 45; export const LOTTO_SIZE = 6; -export const LOTTO_PRICE = 1000; \ No newline at end of file +export const LOTTO_PRICE = 1000; + +// 등수/상금 테이블 +export const PRIZE = { + FIRST: 2000000000, // 6개 + SECOND: 30000000, // 5개 + 보너스 + THIRD: 1500000, // 5개 + FOURTH: 50000, // 4개 + FIFTH: 5000, // 3개 +}; + +// 출력 라벨 +export const RANK_LABEL = { + FIFTH: "3개 일치 (5,000원)", + FOURTH: "4개 일치 (50,000원)", + THIRD: "5개 일치 (1,500,000원)", + SECOND: "5개 일치, 보너스 볼 일치 (30,000,000원)", + FIRST: "6개 일치 (2,000,000,000원)", +}; \ No newline at end of file From 3e2c92f11c5b4037bb3355a432ce9b725ea8e3cc Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:15:22 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=20=EB=B0=9C=ED=96=89=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/service/lottoService.js | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/service/lottoService.js diff --git a/README.md b/README.md index 9c81ce36a..afbeb56e9 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,12 @@ ### 2. domain - [X] Lotto 클래스 : 생성자에서 번호 검증 -- [ ] Ranks, Prizes 상수 : 등수 규칙 및 상금 테이블 +- [X] Ranks, Prizes 상수 : 등수 규칙 및 상금 테이블 ### 3. service - LottoService - - [ ] issueTickets : 금액 -> 정수 계산, Random Api 통한 티켓 발행 + - [X] issueTickets : 금액 -> 정수 계산, Random Api 통한 티켓 발행 - [ ] evaluate : 로또 번호 일치 개수 계산, 등수 판정, 집계 및 수익률 반환 - matchCounter : 교집합 개수 판정 - rankResolver : 보너스 포함 판정 diff --git a/src/service/lottoService.js b/src/service/lottoService.js new file mode 100644 index 000000000..ab5e77fae --- /dev/null +++ b/src/service/lottoService.js @@ -0,0 +1,13 @@ +import {MissionUtils} from '@woowacourse/mission-utils'; +import { LOTTO_MIN, LOTTO_MAX, LOTTO_SIZE, LOTTO_PRICE, PRIZE } from "../constants.js"; +import Lotto from "../domain/Lotto.js"; + +const sortAscending = (arr) => [...arr].sort((a, b) => a - b); + +export const issueTickets = (amount) => { + const count = amount / LOTTO_PRICE; + return Array.from({ length: count }, () => { + const numbers = MissionUtils.Random.pickUniqueNumbersInRange(LOTTO_MIN, LOTTO_MAX, LOTTO_SIZE); + return new Lotto(sortAscending(numbers)); + }); +}; \ No newline at end of file From cde0545cde2b1ebc0c85a08ac3ce1481b4d513d7 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:46:52 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=EB=A1=9C=EB=98=90=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/lottoService.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/service/lottoService.js b/src/service/lottoService.js index ab5e77fae..8ec56bda8 100644 --- a/src/service/lottoService.js +++ b/src/service/lottoService.js @@ -6,8 +6,10 @@ const sortAscending = (arr) => [...arr].sort((a, b) => a - b); export const issueTickets = (amount) => { const count = amount / LOTTO_PRICE; - return Array.from({ length: count }, () => { + const tickets = []; + for (let i = 0; i < count; i += 1) { const numbers = MissionUtils.Random.pickUniqueNumbersInRange(LOTTO_MIN, LOTTO_MAX, LOTTO_SIZE); - return new Lotto(sortAscending(numbers)); - }); + tickets.push(new Lotto(sortAscending(numbers))); + } + return tickets; }; \ No newline at end of file From f0e2132f3ce0c6c7dcc58e64ffa7b0891325c228 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:50:41 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20=EA=B5=90=EC=A7=91=ED=95=A9=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/service/lottoService.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index afbeb56e9..7166c6736 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ ### 3. service - LottoService - [X] issueTickets : 금액 -> 정수 계산, Random Api 통한 티켓 발행 + - [X] matchCounter : 교집합 개수 판정 + - [ ] rankResolver : 보너스 포함 판정 - [ ] evaluate : 로또 번호 일치 개수 계산, 등수 판정, 집계 및 수익률 반환 -- matchCounter : 교집합 개수 판정 -- rankResolver : 보너스 포함 판정 ### 4. io diff --git a/src/service/lottoService.js b/src/service/lottoService.js index 8ec56bda8..59f26f139 100644 --- a/src/service/lottoService.js +++ b/src/service/lottoService.js @@ -12,4 +12,15 @@ export const issueTickets = (amount) => { tickets.push(new Lotto(sortAscending(numbers))); } return tickets; +}; + +export const matchCounter = (ticket, winningNumbers) => { + const winSet = new Set(winningNumbers); + let count = 0; + for (const n of ticket) { + if (winSet.has(n)) { + count += 1; + } + } + return count; }; \ No newline at end of file From 3f9b59748881a0f1b218621b2d50e9e44ad8016f Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:53:31 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20=EB=B3=B4=EB=84=88=EC=8A=A4=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/service/lottoService.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7166c6736..e2e02b370 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ - LottoService - [X] issueTickets : 금액 -> 정수 계산, Random Api 통한 티켓 발행 - [X] matchCounter : 교집합 개수 판정 - - [ ] rankResolver : 보너스 포함 판정 + - [X] rankResolver : 보너스 포함 판정 - [ ] evaluate : 로또 번호 일치 개수 계산, 등수 판정, 집계 및 수익률 반환 diff --git a/src/service/lottoService.js b/src/service/lottoService.js index 59f26f139..309507a54 100644 --- a/src/service/lottoService.js +++ b/src/service/lottoService.js @@ -23,4 +23,14 @@ export const matchCounter = (ticket, winningNumbers) => { } } return count; +}; + +export const rankResolver = (ticketNumbers, winningNumbers, bonusNumber) => { + const matchCount = matchCounter(ticketNumbers, winningNumbers); + if (matchCount === 6) return 'FIRST'; + if (matchCount === 5 && ticketNumbers.includes(bonusNumber)) return 'SECOND'; + if (matchCount === 5) return 'THIRD'; + if (matchCount === 4) return 'FOURTH'; + if (matchCount === 3) return 'FIFTH'; + return 'NONE'; }; \ No newline at end of file From ac1c6bcd8f2ec25381a06a49d7f231bf7bf62a3b Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:58:55 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9D=BC=EC=B9=98=20=EA=B0=9C=EC=88=98=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=B0=8F=20=EB=93=B1=EC=88=98=20=ED=8C=90=EC=A0=95?= =?UTF-8?q?,=20=EC=A7=91=EA=B3=84=20=EB=B0=8F=20=EC=88=98=EC=9D=B5?= =?UTF-8?q?=EB=A5=A0=20=EB=B0=98=ED=99=98=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/service/lottoService.js | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2e02b370..156d819dc 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ - [X] issueTickets : 금액 -> 정수 계산, Random Api 통한 티켓 발행 - [X] matchCounter : 교집합 개수 판정 - [X] rankResolver : 보너스 포함 판정 - - [ ] evaluate : 로또 번호 일치 개수 계산, 등수 판정, 집계 및 수익률 반환 + - [X] evaluate : 로또 번호 일치 개수 계산, 등수 판정, 집계 및 수익률 반환 ### 4. io diff --git a/src/service/lottoService.js b/src/service/lottoService.js index 309507a54..4cb764060 100644 --- a/src/service/lottoService.js +++ b/src/service/lottoService.js @@ -33,4 +33,25 @@ export const rankResolver = (ticketNumbers, winningNumbers, bonusNumber) => { if (matchCount === 4) return 'FOURTH'; if (matchCount === 3) return 'FIFTH'; return 'NONE'; -}; \ No newline at end of file +}; + +const computeTotalPrize = (rankCounts) => { + let total = 0; + total += rankCounts.FIRST * PRIZE.FIRST; + total += rankCounts.SECOND * PRIZE.SECOND; + total += rankCounts.THIRD * PRIZE.THIRD; + total += rankCounts.FOURTH * PRIZE.FOURTH; + total += rankCounts.FIFTH * PRIZE.FIFTH; + return total; +}; + +export const evaluate = (tickets, winningNumbers, bonusNumber) => { + const rankCounts = { FIRST: 0, SECOND: 0, THIRD: 0, FOURTH: 0, FIFTH: 0}; + for (const ticket of tickets) { + const rank = rankResolver(ticket.getNumbers(), winningNumbers, bonusNumber); + if (counts[rank] !== undefined) counts[rank] += 1; + } + return { counts: rankCounts, totalPrize: computeTotalPrize(rankCounts) }; +}; + +export default { issueTickets, matchCounter, rankResolver, evaluate }; \ No newline at end of file From a13438de5bfc7e139b436a5cd4fab94c90768916 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:10:21 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/io/error.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/io/error.js diff --git a/README.md b/README.md index 156d819dc..99f8cc40a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ ### 4. io - [ ] InputView - [ ] OutputView -- [ ] 공통 에러 처리 +- [X] 공통 에러 처리 ### 5. App.js diff --git a/src/io/error.js b/src/io/error.js new file mode 100644 index 000000000..ec7b63f8c --- /dev/null +++ b/src/io/error.js @@ -0,0 +1,20 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; + +export const printError = (error) => { + let raw = error; + if (raw === null || raw === undefined) { + raw = ""; + } + + let msg; + if (raw && typeof raw.message === 'string') { + msg = raw.message; + } else { + msg = String(raw); + } + + if (!msg.startsWith("[ERROR]")) { + msg = `[ERROR] ${msg}`; + } + MissionUtils.Console.print(msg); +}; \ No newline at end of file From 1d6215ace98b695750d11d8985e0271d603ec71d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:18:41 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20InputView=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/io/inputView.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/io/inputView.js diff --git a/README.md b/README.md index 99f8cc40a..4990966ff 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ### 4. io -- [ ] InputView +- [X] InputView - [ ] OutputView - [X] 공통 에러 처리 diff --git a/src/io/inputView.js b/src/io/inputView.js new file mode 100644 index 000000000..a20393585 --- /dev/null +++ b/src/io/inputView.js @@ -0,0 +1,39 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import { parseAmount, parseWinningNumbers, parseBonusNumber } from "../util/parser.js"; +import { validateAmoun, validateWinningNumbers, validateBonusNumber } from '../util/validator.js'; +import { printError } from './error.js'; + +const ask = (question) => MissionUtils.Console.readLineAsync(question); + +export const askPurchaseAmount = async () => { + while (true) { + try { + const amountStr = await ask("구입금액을 입력해 주세요.\n"); + return validateAmount(parseAmount(amountStr)); + } catch (error) { + printError(error); + } + } +}; + +export const askWinningNumbers = async () => { + while (true) { + try { + const numbersStr = await ask("\n당첨 번호를 입력해 주세요.\n"); + return validateWinningNumbers(parseWinningNumbers(numbersStr)); + } catch (error) { + printError(error); + } + } +}; + +export const askBonusNumber = async (winningNumbers) => { + while (true) { + try { + const bonusStr = await ask("\n보너스 번호를 입력해 주세요.\n"); + return validateBonusNumber(parseBonusNumber(bonusStr), winningNumbers); + } catch (error) { + printError(error); + } + } +}; \ No newline at end of file From d0c72cfabbfbde6e9c7bff483fcc4783024bd511 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:28:58 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20outView=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/io/outView.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/io/outView.js diff --git a/README.md b/README.md index 4990966ff..f039c9c94 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ ### 4. io - [X] InputView -- [ ] OutputView +- [X] OutputView - [X] 공통 에러 처리 diff --git a/src/io/outView.js b/src/io/outView.js new file mode 100644 index 000000000..742e10254 --- /dev/null +++ b/src/io/outView.js @@ -0,0 +1,21 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import { formatTicket, formatRate } from '../util/format.js'; +import { RANK_LABEL } from '../constants.js'; + +export const printIssuedTickets = (tickets) => { + MissionUtils.Console.print(`\n${tickets.length}개를 구매했습니다.`); + for (const ticket of tickets) { + MissionUtils.Console.print(formatTicket(ticket.getNumbers())); + } +}; + +export const printEvaluationResults = (rankCounts, totalPrize, purchaseAmount) => { + MissionUtils.Console.print('\n당첨 통계\n---'); + MissionUtils.Console.print(`${RANK_LABEL.FIRST} (${RANK_LABEL.FIRST}원) - ${rankCounts.FIRST}개`); + MissionUtils.Console.print(`${RANK_LABEL.SECOND} (${RANK_LABEL.SECOND}원) - ${rankCounts.SECOND}개`); + MissionUtils.Console.print(`${RANK_LABEL.THIRD} (${RANK_LABEL.THIRD}원) - ${rankCounts.THIRD}개`); + MissionUtils.Console.print(`${RANK_LABEL.FOURTH} (${RANK_LABEL.FOURTH}원) - ${rankCounts.FOURTH}개`); + MissionUtils.Console.print(`${RANK_LABEL.FIFTH} (${RANK_LABEL.FIFTH}원) - ${rankCounts.FIFTH}개`); + const rate = (totalPrize / purchaseAmount) * 100; + MissionUtils.Console.print(`총 수익률은 ${formatRate(rate)}입니다.`); +}; \ No newline at end of file From 5ff174728f4fc8be2f1f94fb6d6eb0cc609bceee Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:06:03 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20App.js=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 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5d..ae1376dd5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,20 @@ +import { askPurchaseAmount, askWinningNumbers, askBonusNumber} from "./io/inputView.js"; +import { printIssuedTickets, printEvaluationResults } from "./io/outView.js"; +import { issueTickets, evaluate } from "./service/lottoService.js"; + + class App { - async run() {} + async run() { + const amount = await askPurchaseAmount(); + const tickets = issueTickets(amount); + printIssuedTickets(tickets); + + const winningNumbers = await askWinningNumbers(); + const bonusNumber = await askBonusNumber(winningNumbers); + + const { counts, totalPrize } = evaluate(tickets, winningNumbers, bonusNumber); + printEvaluationResults(counts, totalPrize, amount); + } } export default App; From 893c8da31c0dbffc642135aa1ad4f9aa0620b247 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:18:54 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/io/inputView.js | 2 +- src/io/outView.js | 21 +++++++++++++-------- src/service/lottoService.js | 5 +++-- src/util/parser.js | 10 ++++++---- src/util/validator.js | 9 ++++----- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/io/inputView.js b/src/io/inputView.js index a20393585..3285e36d8 100644 --- a/src/io/inputView.js +++ b/src/io/inputView.js @@ -1,6 +1,6 @@ import { MissionUtils } from '@woowacourse/mission-utils'; import { parseAmount, parseWinningNumbers, parseBonusNumber } from "../util/parser.js"; -import { validateAmoun, validateWinningNumbers, validateBonusNumber } from '../util/validator.js'; +import { validateAmount, validateWinningNumbers, validateBonusNumber } from '../util/validator.js'; import { printError } from './error.js'; const ask = (question) => MissionUtils.Console.readLineAsync(question); diff --git a/src/io/outView.js b/src/io/outView.js index 742e10254..e9a86ef98 100644 --- a/src/io/outView.js +++ b/src/io/outView.js @@ -2,6 +2,8 @@ import { MissionUtils } from '@woowacourse/mission-utils'; import { formatTicket, formatRate } from '../util/format.js'; import { RANK_LABEL } from '../constants.js'; +const RANK_ORDER = ['FIFTH', 'FOURTH', 'THIRD', 'SECOND', 'FIRST']; + export const printIssuedTickets = (tickets) => { MissionUtils.Console.print(`\n${tickets.length}개를 구매했습니다.`); for (const ticket of tickets) { @@ -10,12 +12,15 @@ export const printIssuedTickets = (tickets) => { }; export const printEvaluationResults = (rankCounts, totalPrize, purchaseAmount) => { - MissionUtils.Console.print('\n당첨 통계\n---'); - MissionUtils.Console.print(`${RANK_LABEL.FIRST} (${RANK_LABEL.FIRST}원) - ${rankCounts.FIRST}개`); - MissionUtils.Console.print(`${RANK_LABEL.SECOND} (${RANK_LABEL.SECOND}원) - ${rankCounts.SECOND}개`); - MissionUtils.Console.print(`${RANK_LABEL.THIRD} (${RANK_LABEL.THIRD}원) - ${rankCounts.THIRD}개`); - MissionUtils.Console.print(`${RANK_LABEL.FOURTH} (${RANK_LABEL.FOURTH}원) - ${rankCounts.FOURTH}개`); - MissionUtils.Console.print(`${RANK_LABEL.FIFTH} (${RANK_LABEL.FIFTH}원) - ${rankCounts.FIFTH}개`); - const rate = (totalPrize / purchaseAmount) * 100; - MissionUtils.Console.print(`총 수익률은 ${formatRate(rate)}입니다.`); + MissionUtils.Console.print('\n당첨 통계'); + MissionUtils.Console.print('---'); + for (const r of RANK_ORDER) { + let c = 0; + if (rankCounts && rankCounts[r] !== undefined) { + c = rankCounts[r]; + } + MissionUtils.Console.print(`${RANK_LABEL[r]} - ${c}개`); + } + const rate = (totalPrize / purchaseAmount) * 100; + MissionUtils.Console.print(`총 수익률은 ${formatRate(rate)}입니다.`); }; \ No newline at end of file diff --git a/src/service/lottoService.js b/src/service/lottoService.js index 4cb764060..9c6477e5a 100644 --- a/src/service/lottoService.js +++ b/src/service/lottoService.js @@ -49,9 +49,10 @@ export const evaluate = (tickets, winningNumbers, bonusNumber) => { const rankCounts = { FIRST: 0, SECOND: 0, THIRD: 0, FOURTH: 0, FIFTH: 0}; for (const ticket of tickets) { const rank = rankResolver(ticket.getNumbers(), winningNumbers, bonusNumber); - if (counts[rank] !== undefined) counts[rank] += 1; + if (rankCounts[rank] !== undefined) rankCounts[rank] += 1; } - return { counts: rankCounts, totalPrize: computeTotalPrize(rankCounts) }; + const totalPrize = computeTotalPrize(rankCounts); + return { rankCounts, totalPrize }; }; export default { issueTickets, matchCounter, rankResolver, evaluate }; \ No newline at end of file diff --git a/src/util/parser.js b/src/util/parser.js index 8e1898397..ea3b6fba6 100644 --- a/src/util/parser.js +++ b/src/util/parser.js @@ -1,15 +1,17 @@ +import { LOTTO_SIZE } from "../constants.js"; + export function parseAmount(amountStr) { const amount = String(amountStr ?? "").trim(); if (!/^\d+$/.test(amount)) { - throw new Error("[Error] 구입 금액은 숫자여야 합니다."); + throw new Error("[ERROR] 구입 금액은 숫자여야 합니다."); } return Number(amount); } export function parseWinningNumbers(numbersStr) { const numbers = String(numbersStr ?? "").trim().split(",").map(num => num.trim()); - if (numbers.length !== 6 || !numbers.every(num => /^\d+$/.test(num))) { - throw new Error("[Error] 당첨 번호는 쉼표로 구분된 6개의 숫자여야 합니다."); + if (numbers.length !== LOTTO_SIZE || numbers.every(num => !/^\d+$/.test(num))) { + throw new Error(`[ERROR] 당첨 번호는 쉼표로 구분된 ${LOTTO_MAX}개의 숫자여야 합니다.`); } return numbers.map(Number); } @@ -17,7 +19,7 @@ export function parseWinningNumbers(numbersStr) { export function parseBonusNumber(bonusStr) { const bonus = String(bonusStr ?? "").trim(); if (!/^\d+$/.test(bonus)) { - throw new Error("[Error] 보너스 번호는 숫자여야 합니다."); + throw new Error("[ERROR] 보너스 번호는 숫자여야 합니다."); } return Number(bonus); } diff --git a/src/util/validator.js b/src/util/validator.js index 6e6663362..13c892d58 100644 --- a/src/util/validator.js +++ b/src/util/validator.js @@ -14,9 +14,9 @@ export const validateAmount = (amount) => { export const validateWinningNumbers = (numbers) => { if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { - throw new Error("[Error] 로또 번호는 6개의 숫자여야 합니다."); + throw new Error(`[Error] 로또 번호는 ${LOTTO_MIN}개의 숫자여야 합니다.`); } - if (!numbers.some(number => !inRange(number))) { + if (numbers.some(number => !inRange(number))) { throw new Error(`[Error] 로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); } const uniqueNumbers = new Set(numbers); @@ -26,13 +26,12 @@ export const validateWinningNumbers = (numbers) => { return numbers; } - export const validateBonusNumber = (bonusNumber, winningNumbers) => { if (!inRange(bonusNumber)) { throw new Error(`[Error] 보너스 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); } - const winningNumbers = new Set(winningNumbers); - if (winningNumbers.includes(bonusNumber)) { + const winSet = new Set(winningNumbers); + if (winSet.has(bonusNumber)) { throw new Error("[Error] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); } return bonusNumber; From 2d7fda79e984361dc982c2b81abef8ba325ba093 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:32:17 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=ED=91=9C?= =?UTF-8?q?=EA=B8=B0=20=EC=A4=91=EB=B3=B5=20=EB=AC=B8=EC=A0=9C=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__/LottoTest.js | 2 +- src/domain/Lotto.js | 6 +++--- src/service/lottoService.js | 2 +- src/util/parser.js | 6 +++--- src/util/validator.js | 14 +++++++------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..e207eb128 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,4 +1,4 @@ -import Lotto from "../src/Lotto"; +import Lotto from "../src/domain/Lotto"; describe("로또 클래스 테스트", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index d0c9eae61..19c1f9ce4 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -10,15 +10,15 @@ class Lotto { #validate(numbers) { if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + throw new Error("로또 번호는 6개여야 합니다."); } const isRange = (number) => Number.isInteger(number) && LOTTO_MIN <= number && number <= LOTTO_MAX; if (numbers.some(number => !isRange(number))) { - throw new Error(`[ERROR] 로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + throw new Error(`로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); } const set = new Set(numbers); if (set.size !== numbers.length) { - throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + throw new Error("로또 번호는 중복될 수 없습니다."); } } diff --git a/src/service/lottoService.js b/src/service/lottoService.js index 9c6477e5a..2f314686e 100644 --- a/src/service/lottoService.js +++ b/src/service/lottoService.js @@ -52,7 +52,7 @@ export const evaluate = (tickets, winningNumbers, bonusNumber) => { if (rankCounts[rank] !== undefined) rankCounts[rank] += 1; } const totalPrize = computeTotalPrize(rankCounts); - return { rankCounts, totalPrize }; + return { counts: rankCounts, totalPrize }; }; export default { issueTickets, matchCounter, rankResolver, evaluate }; \ No newline at end of file diff --git a/src/util/parser.js b/src/util/parser.js index ea3b6fba6..980f7cd55 100644 --- a/src/util/parser.js +++ b/src/util/parser.js @@ -3,7 +3,7 @@ import { LOTTO_SIZE } from "../constants.js"; export function parseAmount(amountStr) { const amount = String(amountStr ?? "").trim(); if (!/^\d+$/.test(amount)) { - throw new Error("[ERROR] 구입 금액은 숫자여야 합니다."); + throw new Error("구입 금액은 숫자여야 합니다."); } return Number(amount); } @@ -11,7 +11,7 @@ export function parseAmount(amountStr) { export function parseWinningNumbers(numbersStr) { const numbers = String(numbersStr ?? "").trim().split(",").map(num => num.trim()); if (numbers.length !== LOTTO_SIZE || numbers.every(num => !/^\d+$/.test(num))) { - throw new Error(`[ERROR] 당첨 번호는 쉼표로 구분된 ${LOTTO_MAX}개의 숫자여야 합니다.`); + throw new Error(`당첨 번호는 쉼표로 구분된 ${LOTTO_MAX}개의 숫자여야 합니다.`); } return numbers.map(Number); } @@ -19,7 +19,7 @@ export function parseWinningNumbers(numbersStr) { export function parseBonusNumber(bonusStr) { const bonus = String(bonusStr ?? "").trim(); if (!/^\d+$/.test(bonus)) { - throw new Error("[ERROR] 보너스 번호는 숫자여야 합니다."); + throw new Error("보너스 번호는 숫자여야 합니다."); } return Number(bonus); } diff --git a/src/util/validator.js b/src/util/validator.js index 13c892d58..50c2bafca 100644 --- a/src/util/validator.js +++ b/src/util/validator.js @@ -4,35 +4,35 @@ const inRange = (number) => Number.isInteger(number) && LOTTO_MIN <= number && n export const validateAmount = (amount) => { if (!Number.isInteger(amount)) { - throw new Error("[Error] 구입 금액은 숫자여야 합니다."); + throw new Error("구입 금액은 숫자여야 합니다."); } if (amount < LOTTO_PRICE || amount % LOTTO_PRICE !== 0) { - throw new Error("[Error] 구입 금액은 1000원 단위여야 합니다."); + throw new Error("구입 금액은 1000원 단위여야 합니다."); } return amount; } export const validateWinningNumbers = (numbers) => { if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { - throw new Error(`[Error] 로또 번호는 ${LOTTO_MIN}개의 숫자여야 합니다.`); + throw new Error(`로또 번호는 ${LOTTO_MIN}개의 숫자여야 합니다.`); } if (numbers.some(number => !inRange(number))) { - throw new Error(`[Error] 로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + throw new Error(`로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); } const uniqueNumbers = new Set(numbers); if (uniqueNumbers.size !== numbers.length) { - throw new Error("[Error] 로또 번호는 중복될 수 없습니다."); + throw new Error("로또 번호는 중복될 수 없습니다."); } return numbers; } export const validateBonusNumber = (bonusNumber, winningNumbers) => { if (!inRange(bonusNumber)) { - throw new Error(`[Error] 보너스 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + throw new Error(`보너스 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); } const winSet = new Set(winningNumbers); if (winSet.has(bonusNumber)) { - throw new Error("[Error] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + throw new Error("보너스 번호는 당첨 번호와 중복될 수 없습니다."); } return bonusNumber; } \ No newline at end of file From 0567c011e8212ca553f7a2f2b40fa6d433a5f755 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:37:05 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/LottoTest.js | 2 +- src/Lotto.js | 30 ++++++++++++++++++++++++++++++ src/domain/Lotto.js | 6 +++--- src/service/lottoService.js | 2 +- src/util/parser.js | 8 ++++---- src/util/validator.js | 2 +- 6 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 src/Lotto.js diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index e207eb128..409aaf69b 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,4 +1,4 @@ -import Lotto from "../src/domain/Lotto"; +import Lotto from "../src/Lotto"; describe("로또 클래스 테스트", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { diff --git a/src/Lotto.js b/src/Lotto.js new file mode 100644 index 000000000..cf2aa0d7d --- /dev/null +++ b/src/Lotto.js @@ -0,0 +1,30 @@ +import { LOTTO_MIN, LOTTO_MAX, LOTTO_SIZE } from "./constants.js"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = [...numbers].sort((a, b) => a - b); + } + + #validate(numbers) { + if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { + throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + } + const isRange = (number) => Number.isInteger(number) && LOTTO_MIN <= number && number <= LOTTO_MAX; + if (numbers.some(number => !isRange(number))) { + throw new Error(`[ERROR] 로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + } + const set = new Set(numbers); + if (set.size !== numbers.length) { + throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + } + + getNumbers() { + return [...this.#numbers]; + } +} + +export default Lotto; diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 19c1f9ce4..d0c9eae61 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -10,15 +10,15 @@ class Lotto { #validate(numbers) { if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { - throw new Error("로또 번호는 6개여야 합니다."); + throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } const isRange = (number) => Number.isInteger(number) && LOTTO_MIN <= number && number <= LOTTO_MAX; if (numbers.some(number => !isRange(number))) { - throw new Error(`로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); + throw new Error(`[ERROR] 로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); } const set = new Set(numbers); if (set.size !== numbers.length) { - throw new Error("로또 번호는 중복될 수 없습니다."); + throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); } } diff --git a/src/service/lottoService.js b/src/service/lottoService.js index 2f314686e..f6f04377c 100644 --- a/src/service/lottoService.js +++ b/src/service/lottoService.js @@ -1,6 +1,6 @@ import {MissionUtils} from '@woowacourse/mission-utils'; import { LOTTO_MIN, LOTTO_MAX, LOTTO_SIZE, LOTTO_PRICE, PRIZE } from "../constants.js"; -import Lotto from "../domain/Lotto.js"; +import Lotto from "../Lotto.js"; const sortAscending = (arr) => [...arr].sort((a, b) => a - b); diff --git a/src/util/parser.js b/src/util/parser.js index 980f7cd55..b0576e0f9 100644 --- a/src/util/parser.js +++ b/src/util/parser.js @@ -9,11 +9,11 @@ export function parseAmount(amountStr) { } export function parseWinningNumbers(numbersStr) { - const numbers = String(numbersStr ?? "").trim().split(",").map(num => num.trim()); - if (numbers.length !== LOTTO_SIZE || numbers.every(num => !/^\d+$/.test(num))) { - throw new Error(`당첨 번호는 쉼표로 구분된 ${LOTTO_MAX}개의 숫자여야 합니다.`); + const tokens = String(numbersStr ?? "").trim().split(",").map(num => num.trim()); + if (tokens.length !== LOTTO_SIZE || tokens.some(token => !/^\d+$/.test(token))) { + throw new Error(`당첨 번호는 쉼표로 구분된 ${LOTTO_SIZE}개의 숫자여야 합니다.`); } - return numbers.map(Number); + return tokens.map(Number); } export function parseBonusNumber(bonusStr) { diff --git a/src/util/validator.js b/src/util/validator.js index 50c2bafca..23faf7359 100644 --- a/src/util/validator.js +++ b/src/util/validator.js @@ -14,7 +14,7 @@ export const validateAmount = (amount) => { export const validateWinningNumbers = (numbers) => { if (!Array.isArray(numbers) || numbers.length !== LOTTO_SIZE) { - throw new Error(`로또 번호는 ${LOTTO_MIN}개의 숫자여야 합니다.`); + throw new Error(`로또 번호는 ${LOTTO_SIZE}개의 숫자여야 합니다.`); } if (numbers.some(number => !inRange(number))) { throw new Error(`로또 번호는 ${LOTTO_MIN}부터 ${LOTTO_MAX} 사이의 숫자여야 합니다.`); From bcb1be4c20b75e2c6b8108ba040b907f31f77087 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:51:15 +0900 Subject: [PATCH 20/20] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=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__/LottoTest.js | 50 ++++++++++++++++++++++++++++++++++++++-- __tests__/ServiceTest.js | 38 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 __tests__/ServiceTest.js diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..621345ac2 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -7,12 +7,58 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); }).toThrow("[ERROR]"); }); - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + test("범위 밖(0 포함)이면 예외", () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + }); + + test("범위 밖(46 포함)이면 예외", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR]"); + }); + + test("정수가 아니면(소수 포함) 예외", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6.5]); + }).toThrow("[ERROR]"); + }); + + test("정수가 아니면(문자열 포함) 예외", () => { + expect(() => { + // 문자열 '1' 포함 → 정수 아님 → 예외 + new Lotto(["1", 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + }); + + test("내부 저장은 오름차순으로 정렬된다.", () => { + const lotto = new Lotto([6, 1, 3, 2, 5, 4]); + expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5, 6]); + }); + + test("getNumbers는 불변성을 보장한다(복사본 반환).", () => { + const lotto = new Lotto([1, 2, 3, 4, 5, 6]); + const arr = lotto.getNumbers(); + arr[0] = 999; + expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5, 6]); + }); + + test("경계값 1과 45는 유효하다.", () => { + expect(() => { + new Lotto([1, 3, 10, 20, 30, 45]); + }).not.toThrow(); + }); + + test("로또 번호의 개수가 6개 미만이면 예외가 발생한다.", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5]); + }).toThrow("[ERROR]"); + }); }); diff --git a/__tests__/ServiceTest.js b/__tests__/ServiceTest.js new file mode 100644 index 000000000..c9a779162 --- /dev/null +++ b/__tests__/ServiceTest.js @@ -0,0 +1,38 @@ +import { rankResolver, evaluate } from "../src/service/lottoService.js"; +import Lotto from "../src/Lotto"; +import { PRIZE } from "../src/constants.js"; + +describe("서비스 로직 테스트", () => { + test("rankResolver: 5개 + 보너스 일치면 SECOND", () => { + const ticket = new Lotto([1, 2, 3, 4, 5, 7]); // 보너스 7 포함 + const winning = [1, 2, 3, 4, 5, 6]; + const bonus = 7; + const rank = rankResolver(ticket.getNumbers(), winning, bonus); + expect(rank).toBe("SECOND"); + }); + + test("evaluate: 집계/상금 합계 계산", () => { + const tickets = [ + new Lotto([1, 2, 3, 4, 5, 6]), // FIRST + new Lotto([1, 2, 3, 4, 5, 7]), // SECOND (보너스 7) + new Lotto([1, 2, 3, 4, 10, 11]), // FOURTH + new Lotto([1, 2, 3, 20, 21, 22]), // FIFTH + new Lotto([40, 41, 42, 43, 44, 45]), // NONE + ]; + const winning = [1, 2, 3, 4, 5, 6]; + const bonus = 7; + + const { counts, totalPrize } = evaluate(tickets, winning, bonus); + + expect(counts).toEqual({ + FIRST: 1, + SECOND: 1, + THIRD: 0, + FOURTH: 1, + FIFTH: 1, + }); + + const expected = PRIZE.FIRST + PRIZE.SECOND + PRIZE.FOURTH + PRIZE.FIFTH; + expect(totalPrize).toBe(expected); + }); +});