From 78fa32c032e50e2e9814332689de43c7133bdd32 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Sun, 2 Nov 2025 18:17:42 +0900 Subject: [PATCH 1/9] docs: write a draft README.md --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/README.md b/README.md index 15bb106b5..e5aaaa4c4 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ # javascript-lotto-precourse + +로또 + +## 프로젝트 개요 + +- 목표: 자세하게 설명 예정 +- 패턴: MVC (Model-View-Controller) + +- 프로젝트 개요 및 도전할 테스트 케이스 정리 +- 기능적 요소 정리 + +## 기능 요구 사항 + +- 기술 예정 + +## 아키텍쳐 + +``` +MVC 패턴 +📁 src + ├── App.js # 프로그램 시작점 (run()) + ├── controller/ + │ └── LottoController.js # 사용자 입력/출력 흐름 제어 + ├── model/ + │ ├── Lotto.js # 한 장 로또, 번호 관리 + │ ├── LottoStore.js # 여러 장 로또 관리 + │ └── PrizeCalculator.js # 당첨 결과 계산 및 수익률 + ├── view/ + │ ├── InputView.js # 사용자 입력 + │ └── OutputView.js # 출력 + └── constants/ + └── LottoConfig.js # 번호 범위, 가격, 상금, 등수 설정 +``` + +- Controller: InputView → Model → OutputView 연결, 에러 처리 및 흐름 제어 +- Model: Lotto, LottoStore, PrizeCalculator (SRP 준수) +- View: InputView / OutputView → 단위 테스트용 Mock 가능 +- Constants: 재사용 가능한 설정 관리 + +| 브랜치 이름 | 담당 기능 | 상세 내용 | 테스트 포인트 | +| ---------------------------- | ------------------ | --------------------------------------------------------- | --------------------------------------------- | +| `feature/set-up` | 기본 구조 생성 | MVC 패턴 별 폴더, 파일 생성 | 기본적인 프로젝트 생성 | +| `feature/purchase-input` | 구입 금액 입력 | 1000원 단위 입력, 잘못된 입력 시 `[ERROR]` 출력 후 재입력 | 금액 범위, 1000원 단위 확인, 재입력 흐름 | +| `feature/lotto-generation` | 로또 발행 | 1~45 범위, 중복 없는 6개 번호 생성, N장 구매 시 N개 생성 | 번호 개수, 범위, 중복, 오름차순 정렬 | +| `feature/winning-input` | 당첨 번호 입력 | 쉼표 구분 6개 숫자 입력, 범위 1~45, 중복 불가 | 번호 개수, 범위, 중복, 재입력 흐름 | +| `feature/result-calculation` | 당첨 결과 & 수익률 | 등수 판정, 당첨 개수 계산, 총 수익률 계산 | 등수 판정 로직, 보너스 번호 판정, 수익률 계산 | +| `feature/error-handling` | 예외 처리 | 금액, 번호, 보너스 입력 오류 처리, `[ERROR]` 메시지 통일 | Error 메시지 테스트, 재입력 흐름 검증 | +| `feature/io-abstraction` | 입출력 추상화 | InputView / OutputView, Console 직접 호출 제거, Mock 가능 | Input/Output Mock 테스트, 출력 포맷 확인 | + +## 코드적 요소 +| 요소 | 상세 내용 | +| ------------- | ---------------------------------------------------------- | +| 객체지향 | Lotto, LottoStore, PrizeCalculator 클래스별 단일 책임(SRP) | +| SRP | 클래스/메서드별 단일 책임 유지, 15줄 이하 | +| 에러 처리 | `[ERROR]` 메시지 일관성, 타입별 Error 분리 가능 | +| 입출력 추상화 | InputView / OutputView 인터페이스 적용 → Mocking 가능 | +| 설정화 | LottoConfig → 번호 범위, 가격, 상금, 등수 관리 | + +요구사항: SRP, 함수 길이 15줄 이하, 3항 연산자 사용 금지, 함수형 프로그래밍 일부 적용 \ No newline at end of file From 70fc00558b99b5c14bf1d67cba029fb9f7ca0a30 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Sun, 2 Nov 2025 18:45:10 +0900 Subject: [PATCH 2/9] feat(setup): project set-up --- src/LottoConfig.js | 0 src/controller/LottoController.js | 0 src/{ => model}/Lotto.js | 0 src/model/LottoStore.js | 0 src/model/PrizeCalculator.js | 0 src/view/InputView.js | 0 src/view/OutputView.js | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/LottoConfig.js create mode 100644 src/controller/LottoController.js rename src/{ => model}/Lotto.js (100%) create mode 100644 src/model/LottoStore.js create mode 100644 src/model/PrizeCalculator.js create mode 100644 src/view/InputView.js create mode 100644 src/view/OutputView.js diff --git a/src/LottoConfig.js b/src/LottoConfig.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/Lotto.js b/src/model/Lotto.js similarity index 100% rename from src/Lotto.js rename to src/model/Lotto.js diff --git a/src/model/LottoStore.js b/src/model/LottoStore.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/model/PrizeCalculator.js b/src/model/PrizeCalculator.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 000000000..e69de29bb From 2e4ebed11a73797b4db72562205014a6c8496337 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Mon, 3 Nov 2025 17:05:53 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat(purchase-input):=20=EB=A1=9C=EB=98=90?= =?UTF-8?q?=20=EA=B5=AC=EC=9E=85=20=EA=B8=88=EC=95=A1=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EB=B0=8F=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=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 - 사용자의 구입 금액 입력 기능 구현 (InputView) - 출력 기능 구현 (OutputView) - 숫자 입력 및 1,000원 단위 유효성 검사 추가 - LottoController와 입력 흐름 연동 - 잘못된 입력에 대한 예외 처리 추가 --- src/App.js | 7 ++++++- src/LottoConfig.js | 3 +++ src/controller/LottoController.js | 35 +++++++++++++++++++++++++++++++ src/view/InputView.js | 10 +++++++++ src/view/OutputView.js | 12 +++++++++++ 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5d..bf9ee4f5b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import LottoController from "./controller/LottoController.js"; + class App { - async run() {} + async run() { + const controller = new LottoController(); + await controller.getPurchaseAmount(); + } } export default App; diff --git a/src/LottoConfig.js b/src/LottoConfig.js index e69de29bb..517282f00 100644 --- a/src/LottoConfig.js +++ b/src/LottoConfig.js @@ -0,0 +1,3 @@ +export const LOTTO_CONFIG = { + PRICE_PER_TICKET: 1000, +}; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index e69de29bb..37e27f162 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -0,0 +1,35 @@ +import InputView from "../view/InputView.js"; +import OutputView from "../view/OutputView.js"; +import { LOTTO_CONFIG } from "../LottoConfig.js"; + +class LottoController { + async getPurchaseAmount() { + try { + const input = await InputView.readPurchaseAmount(); + const amount = this.#validateAmount(input); + const count = amount / LOTTO_CONFIG.PRICE_PER_TICKET; + OutputView.printPurchaseResult(count); + return count; + } catch (error) { + OutputView.printError(error.message); + } + } + + #validateAmount(input) { + const amount = Number(input); + + if (Number.isNaN(amount)) { + throw new Error("[ERROR] 구입 금액은 숫자여야 합니다."); + } + if (amount <= 0) { + throw new Error("[ERROR] 구입 금액은 0보다 커야 합니다."); + } + if (amount % LOTTO_CONFIG.PRICE_PER_TICKET !== 0) { + throw new Error("[ERROR] 금액은 1000원 단위여야 합니다."); + } + + return amount; + } +} + +export default LottoController; diff --git a/src/view/InputView.js b/src/view/InputView.js index e69de29bb..3b60f2df3 100644 --- a/src/view/InputView.js +++ b/src/view/InputView.js @@ -0,0 +1,10 @@ +import { Console } from "@woowacourse/mission-utils"; + +const InputView = { + async readPurchaseAmount() { + const input = await Console.readLineAsync("구입금액을 입력해 주세요.\n"); + return input; + }, +}; + +export default InputView; \ No newline at end of file diff --git a/src/view/OutputView.js b/src/view/OutputView.js index e69de29bb..81bc12391 100644 --- a/src/view/OutputView.js +++ b/src/view/OutputView.js @@ -0,0 +1,12 @@ +import { Console } from "@woowacourse/mission-utils"; + +const OutputView = { + printError(message) { + Console.print(message); + }, + printPurchaseResult(count) { + Console.print(`${count}개를 구매했습니다.`); + }, +}; + +export default OutputView; From 852b4a37223d722d375263ec0b6ffda80c450cf5 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Mon, 3 Nov 2025 18:14:53 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat(lotto-generation):=20=EB=A1=9C?= =?UTF-8?q?=EB=98=90=20=EB=B0=9C=ED=96=89=20=EB=B0=8F=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LottoController에 issueLottos() 메서드 추가로 로또 생성 및 출력 기능 구현 - LottoStore 모델 생성 및 mission-utils Random API로 번호 무작위 생성 적용 - Lotto 클래스에 번호 검증, 정렬, 일치 개수 계산, 보너스 확인 로직 추가 - OutputView에서 로또 구매 결과 및 발행된 로또 번호 출력 구현 - 전체 흐름(App.js): 구입 금액 입력 → 로또 발행 → 출력 순서 --- src/App.js | 18 +++++++-- src/LottoConfig.js | 17 ++++++++ src/controller/LottoController.js | 40 ++++++++++++++----- src/model/Lotto.js | 66 ++++++++++++++++++++++++++++--- src/model/LottoStore.js | 38 ++++++++++++++++++ src/view/OutputView.js | 22 ++++++++--- 6 files changed, 178 insertions(+), 23 deletions(-) diff --git a/src/App.js b/src/App.js index bf9ee4f5b..acedabc69 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,20 @@ -import LottoController from "./controller/LottoController.js"; +// src/App.js + +import LottoController from './controller/LottoController.js'; class App { + #lottoController; + + constructor() { + this.#lottoController = new LottoController(); + } + async run() { - const controller = new LottoController(); - await controller.getPurchaseAmount(); + // 1. 구입 금액 입력 및 로또 개수 반환 + const count = await this.#lottoController.getPurchaseAmount(); + // 2. 로또 발행 및 출력 + this.#lottoController.issueLottos(count); } } -export default App; +export default App; \ No newline at end of file diff --git a/src/LottoConfig.js b/src/LottoConfig.js index 517282f00..54e0e8890 100644 --- a/src/LottoConfig.js +++ b/src/LottoConfig.js @@ -1,3 +1,20 @@ export const LOTTO_CONFIG = { + MIN_NUMBER: 1, + MAX_NUMBER: 45, + NUMBER_COUNT: 6, PRICE_PER_TICKET: 1000, }; + +// 에러 메시지 정리 +export const ERROR_MESSAGES = { + // 구입 금액 관련 에러 + AMOUNT_NAN: '[ERROR] 구입 금액은 숫자여야 합니다.', + AMOUNT_NEGATIVE: '[ERROR] 구입 금액은 0보다 커야 합니다.', + AMOUNT_UNIT: `[ERROR] 금액은 ${LOTTO_CONFIG.PRICE_PER_TICKET}원 단위여야 합니다.`, + + // 로또 번호 관련 에러 (Lotto.js에서 사용) + LOTTO_LENGTH: `[ERROR] 로또 번호는 ${LOTTO_CONFIG.NUMBER_COUNT}개여야 합니다.`, + LOTTO_DUPLICATE: '[ERROR] 로또 번호에 중복된 숫자가 있습니다.', + LOTTO_RANGE: `[ERROR] 로또 번호는 ${LOTTO_CONFIG.MIN_NUMBER}부터 ${LOTTO_CONFIG.MAX_NUMBER} 사이의 숫자여야 합니다.`, + LOTTO_NOT_INTEGER: '[ERROR] 로또 번호는 정수여야 합니다.', +}; \ No newline at end of file diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 37e27f162..a2244ea4b 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,17 +1,25 @@ -import InputView from "../view/InputView.js"; -import OutputView from "../view/OutputView.js"; -import { LOTTO_CONFIG } from "../LottoConfig.js"; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; +import LottoStore from '../model/LottoStore.js'; +import { LOTTO_CONFIG, ERROR_MESSAGES } from '../LottoConfig.js'; class LottoController { + #lottoStore; + + constructor() { + this.#lottoStore = new LottoStore(); + } + async getPurchaseAmount() { - try { + while (true) { + try { const input = await InputView.readPurchaseAmount(); const amount = this.#validateAmount(input); const count = amount / LOTTO_CONFIG.PRICE_PER_TICKET; - OutputView.printPurchaseResult(count); return count; } catch (error) { OutputView.printError(error.message); + } } } @@ -19,17 +27,31 @@ class LottoController { const amount = Number(input); if (Number.isNaN(amount)) { - throw new Error("[ERROR] 구입 금액은 숫자여야 합니다."); + throw new Error(ERROR_MESSAGES.AMOUNT_NAN); } if (amount <= 0) { - throw new Error("[ERROR] 구입 금액은 0보다 커야 합니다."); + throw new Error(ERROR_MESSAGES.AMOUNT_NEGATIVE); } if (amount % LOTTO_CONFIG.PRICE_PER_TICKET !== 0) { - throw new Error("[ERROR] 금액은 1000원 단위여야 합니다."); + throw new Error(ERROR_MESSAGES.AMOUNT_UNIT); } return amount; } + + /** + @param {number} count + */ + issueLottos(count) { + // 1. 모델(LottoStore)에 로또 생성 요청 + this.#lottoStore.generateLottos(count); + + // 2. 모델에서 생성된 로또 목록 가져오기 + const lottos = this.#lottoStore.getLottos(); + + // 3. 뷰(OutputView)에 출력 요청 + OutputView.printLottos(lottos); + } } -export default LottoController; +export default LottoController; \ No newline at end of file diff --git a/src/model/Lotto.js b/src/model/Lotto.js index cb0b1527e..f19865d60 100644 --- a/src/model/Lotto.js +++ b/src/model/Lotto.js @@ -1,18 +1,74 @@ +import { LOTTO_CONFIG, ERROR_MESSAGES } from '../LottoConfig.js'; + class Lotto { #numbers; constructor(numbers) { + // 기본 검증 this.#validate(numbers); - this.#numbers = numbers; + // 유효성 검사 통과 시, 오름차순으로 정렬 저장 + this.#numbers = numbers.sort((a, b) => a - b); } #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + // 1. 기본 검증 + if (numbers.length !== LOTTO_CONFIG.NUMBER_COUNT) { + throw new Error(ERROR_MESSAGES.LOTTO_LENGTH); + } + + // 2.중복 검증 + if (new Set(numbers).size !== LOTTO_CONFIG.NUMBER_COUNT) { + throw new Error(ERROR_MESSAGES.LOTTO_DUPLICATE); + } + + // 3. 개별 번호의 범위 및 타입 검증 + numbers.forEach((number) => { + this.#validateNumber(number); + }); + } + + // 개별 숫자를 검증하는 private 메서드 + #validateNumber(number) { + if (number < LOTTO_CONFIG.MIN_NUMBER || number > LOTTO_CONFIG.MAX_NUMBER) { + throw new Error(ERROR_MESSAGES.LOTTO_RANGE); + } + if (!Number.isInteger(number)) { + throw new Error(ERROR_MESSAGES.LOTTO_NOT_INTEGER); } } - // TODO: 추가 기능 구현 + //[추가] 로또 번호를 외부(View, Calculator)에서 읽을 수 있도록 getter 제공 + + /** + @returns {number[]} - 정렬된 로또 번호 + */ + getNumbers() { + return this.#numbers; + } + + /** + //당첨 번호와 몇 개가 일치하는지 계산 (결과 계산 시 필요) + @param {number[]} winningNumbers - 당첨 번호 6개 + @returns {number} - 일치하는 번호 개수 + */ + countMatch(winningNumbers) { + const winningSet = new Set(winningNumbers); + + const matchCount = this.#numbers.filter((number) => + winningSet.has(number) + ).length; + + return matchCount; + } + + /** + // 보너스 번호를 포함하는지 확인 (결과 계산 시 필요) + @param {number} bonusNumber - 보너스 번호 + @returns {boolean} - 포함 여부 + */ + hasBonus(bonusNumber) { + return this.#numbers.includes(bonusNumber); + } } -export default Lotto; +export default Lotto; \ No newline at end of file diff --git a/src/model/LottoStore.js b/src/model/LottoStore.js index e69de29bb..730ce517d 100644 --- a/src/model/LottoStore.js +++ b/src/model/LottoStore.js @@ -0,0 +1,38 @@ +// src/model/LottoStore.js + +import { Random } from '@woowacourse/mission-utils'; +import { LOTTO_CONFIG } from '../LottoConfig.js'; +import Lotto from './Lotto.js'; + +class LottoStore { + #lottos; + + constructor() { + this.#lottos = []; + } + + // 로또 생성 + generateLottos(count) { + for (let i = 0; i < count; i++) { + const numbers = this.#pickLottoNumbers(); + const lotto = new Lotto(numbers); + this.#lottos.push(lotto); + } + } + + // mission-utils 사용 + #pickLottoNumbers() { + return Random.pickUniqueNumbersInRange( + LOTTO_CONFIG.MIN_NUMBER, + LOTTO_CONFIG.MAX_NUMBER, + LOTTO_CONFIG.NUMBER_COUNT + ); + } + + // 생성 로또 목록을 반환 get + getLottos() { + return this.#lottos; + } +} + +export default LottoStore; \ No newline at end of file diff --git a/src/view/OutputView.js b/src/view/OutputView.js index 81bc12391..2661b7d08 100644 --- a/src/view/OutputView.js +++ b/src/view/OutputView.js @@ -1,12 +1,24 @@ -import { Console } from "@woowacourse/mission-utils"; +import { Console } from '@woowacourse/mission-utils'; const OutputView = { + printPurchaseResult(count) { + Console.print(`\n${count}개를 구매했습니다.`); + }, + + /** + @param {Lotto[]} lottos + */ + printLottos(lottos) { + lottos.forEach((lotto) => { + const numbers = lotto.getNumbers(); + Console.print(`[${numbers.join(', ')}]`); + }); + }, + printError(message) { Console.print(message); }, - printPurchaseResult(count) { - Console.print(`${count}개를 구매했습니다.`); - }, + }; -export default OutputView; +export default OutputView; \ No newline at end of file From ebbed1d94a38eb36e68368d06104756f1335146e Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Mon, 3 Nov 2025 20:40:20 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat(feature/winning-input):=20=EB=8B=B9?= =?UTF-8?q?=EC=B2=A8=20=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=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 - LottoController에 당첨 번호 관련 메서드 추가 - LottoController 보너스 번호 유효성 검사 - InputView 당첨, 보너스 번호 입력 추가 - LOTTO_CONFIG.js 에러 메시지 추가 - 전체 흐름(App.js): 로또 발행 → 당첨 번호 → 보너스 번호 --- src/App.js | 8 +++ src/LottoConfig.js | 9 ++++ src/controller/LottoController.js | 86 ++++++++++++++++++++++++++++++- src/view/InputView.js | 18 ++++++- 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/App.js b/src/App.js index acedabc69..908e18b18 100644 --- a/src/App.js +++ b/src/App.js @@ -12,8 +12,16 @@ class App { async run() { // 1. 구입 금액 입력 및 로또 개수 반환 const count = await this.#lottoController.getPurchaseAmount(); + // 2. 로또 발행 및 출력 this.#lottoController.issueLottos(count); + + // 3. 당첨 번호 입력 + const winningNumbers = await this.#lottoController.getWinningNumbers(); + + // 4. 보너스 번호 입력 (당첨 번호와 중복 검사를 위해 winningNumbers 전달) + const bonusNumber = await this.#lottoController.getBonusNumber(winningNumbers); + } } diff --git a/src/LottoConfig.js b/src/LottoConfig.js index 54e0e8890..0248c316d 100644 --- a/src/LottoConfig.js +++ b/src/LottoConfig.js @@ -17,4 +17,13 @@ export const ERROR_MESSAGES = { LOTTO_DUPLICATE: '[ERROR] 로또 번호에 중복된 숫자가 있습니다.', LOTTO_RANGE: `[ERROR] 로또 번호는 ${LOTTO_CONFIG.MIN_NUMBER}부터 ${LOTTO_CONFIG.MAX_NUMBER} 사이의 숫자여야 합니다.`, LOTTO_NOT_INTEGER: '[ERROR] 로또 번호는 정수여야 합니다.', + + + // 당첨 번호 파싱 관련 (쉼표로 구분되지 않거나 숫자가 아닌 경우) + WINNING_NOT_NUMBER: '[ERROR] 당첨 번호는 쉼표로 구분된 숫자여야 합니다.', + + // 보너스 번호 관련 에러 + BONUS_NAN: '[ERROR] 보너스 번호는 숫자여야 합니다.', + BONUS_RANGE: `[ERROR] 보너스 번호는 ${LOTTO_CONFIG.MIN_NUMBER}부터 ${LOTTO_CONFIG.MAX_NUMBER} 사이의 숫자여야 합니다.`, + BONUS_DUPLICATE: '[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.', }; \ No newline at end of file diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index a2244ea4b..435bbfde3 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -2,6 +2,7 @@ import InputView from '../view/InputView.js'; import OutputView from '../view/OutputView.js'; import LottoStore from '../model/LottoStore.js'; import { LOTTO_CONFIG, ERROR_MESSAGES } from '../LottoConfig.js'; +import Lotto from "../model/Lotto.js" class LottoController { #lottoStore; @@ -52,6 +53,89 @@ class LottoController { // 3. 뷰(OutputView)에 출력 요청 OutputView.printLottos(lottos); } + + // 당첨 번호 관련 메서드 + /** + 당첨 번호 입력을 받고 유효성 검사를 통과할 때까지 반복 + @returns {Promise} - 유효성이 검증된 당첨 번호 배열 + */ + async getWinningNumbers() { + while (true) { + try { + const input = await InputView.readWinningNumbers(); + const numbers = this.#parseAndValidateWinningNumbers(input); + return numbers; + } catch (error) { + OutputView.printError(error.message); + } + } + } + + /** + 쉼표(,)로 구분된 문자열을 파싱하고 Lotto 모델을 통해 검증 + @param {string} input - 사용자 입력 문자열 + @returns {number[]} - 숫자 배열 + */ + #parseAndValidateWinningNumbers(input) { + const numbers = input.split(',').map((numStr) => { + const num = Number(numStr.trim()); // 공백 제거 후 숫자로 변환 + if (Number.isNaN(num)) { + // Lotto 생성자 전에 NaN 체크가 필요 + throw new Error(ERROR_MESSAGES.WINNING_NOT_NUMBER); + } + return num; + }); + + // Lotto 클래스의 생성자/유효성 검사 로직을 재사용 + // (길이, 중복, 범위, 정수 모두 검사됨) + new Lotto(numbers); + + return numbers; + } + + // 보너스 번호 관련 메서드 + + /** + 보너스 번호 입력을 받고 유효성 검사를 통과할 때까지 반복 + @param {number[]} winningNumbers - (중복 검사를 위한) 당첨 번호 배열 + @returns {Promise} - 유효성이 검증된 보너스 번호 + */ + async getBonusNumber(winningNumbers) { + while (true) { + try { + const input = await InputView.readBonusNumber(); + const number = this.#parseAndValidateBonusNumber(input, winningNumbers); + return number; + } catch (error) { + OutputView.printError(error.message); + } + } + } + + /** + 보너스 번호 문자열을 파싱하고 유효성 검사 + @param {string} input - 사용자 입력 문자열 + @param {number[]} winningNumbers - 당첨 번호 배열 + @returns {number} - 유효한 보너스 번호 + */ + #parseAndValidateBonusNumber(input, winningNumbers) { + const number = Number(input.trim()); + + // 숫자가 아니거나 정수가 아닐 경우 (소수, 문자 등) + if (Number.isNaN(number) || !Number.isInteger(number)) { + throw new Error(ERROR_MESSAGES.BONUS_NAN); + } + // 로또 번호의 유효 범위(예: 1~45)를 벗어나는 경우 + if (number < LOTTO_CONFIG.MIN_NUMBER || number > LOTTO_CONFIG.MAX_NUMBER) { + throw new Error(ERROR_MESSAGES.BONUS_RANGE); + } + // 이미 당첨 번호 배열에 포함된 숫자인 경우 (중복 방지 + if (winningNumbers.includes(number)) { + throw new Error(ERROR_MESSAGES.BONUS_DUPLICATE); + } + + return number; + } } -export default LottoController; \ No newline at end of file +export default LottoController; diff --git a/src/view/InputView.js b/src/view/InputView.js index 3b60f2df3..3717de2d2 100644 --- a/src/view/InputView.js +++ b/src/view/InputView.js @@ -5,6 +5,22 @@ const InputView = { const input = await Console.readLineAsync("구입금액을 입력해 주세요.\n"); return input; }, + + + // 당첨 번호 입력 + async readWinningNumbers() { + // 실행 예시에 따라, 로또 목록 출력 후 한 줄 띄고 질문합니다. + const input = await Console.readLineAsync('\n당첨 번호를 입력해 주세요.\n'); + return input; + }, + + + // 보너스 번호 입력 + async readBonusNumber() { + // 당첨 번호 입력 후 한 줄 띄고 질문합니다. + const input = await Console.readLineAsync('\n보너스 번호를 입력해 주세요.\n'); + return input; + }, }; -export default InputView; \ No newline at end of file +export default InputView; From 9aef29c28439013dc7d53815bbc3afdb967e7373 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Mon, 3 Nov 2025 21:00:12 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat(feature/winning-input):=20=EB=8B=B9?= =?UTF-8?q?=EC=B2=A8=20=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=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 - LottoController 당첨 결과를 계산하고 출력을 요청 - PrizeCalculator.js 구현 - OutPutView - LOTTO_CONFIG.js 당첨 등수, 당첨금 추가 - 전체 흐름(App.js): 결과 계산 및 출력 추가 --- src/App.js | 2 + src/LottoConfig.js | 19 ++++++++ src/controller/LottoController.js | 38 ++++++++++++++- src/model/PrizeCalculator.js | 79 +++++++++++++++++++++++++++++++ src/view/OutputView.js | 34 +++++++++++-- 5 files changed, 167 insertions(+), 5 deletions(-) diff --git a/src/App.js b/src/App.js index 908e18b18..f6ee0f534 100644 --- a/src/App.js +++ b/src/App.js @@ -22,6 +22,8 @@ class App { // 4. 보너스 번호 입력 (당첨 번호와 중복 검사를 위해 winningNumbers 전달) const bonusNumber = await this.#lottoController.getBonusNumber(winningNumbers); + // 5. 결과 계산 및 출력 + this.#lottoController.calculateAndShowResults(winningNumbers, bonusNumber); } } diff --git a/src/LottoConfig.js b/src/LottoConfig.js index 0248c316d..ecd9417b4 100644 --- a/src/LottoConfig.js +++ b/src/LottoConfig.js @@ -5,6 +5,25 @@ export const LOTTO_CONFIG = { PRICE_PER_TICKET: 1000, }; +// 당첨 등수 식별자 +export const RANK = { + FIRST: 'FIRST', + SECOND: 'SECOND', + THIRD: 'THIRD', + FOURTH: 'FOURTH', + FIFTH: 'FIFTH', +}; + +// 등수별 당첨금 +export const PRIZE_MONEY = { + [RANK.FIRST]: 2_000_000_000, + [RANK.SECOND]: 30_000_000, + [RANK.THIRD]: 1_500_000, + [RANK.FOURTH]: 50_000, + [RANK.FIFTH]: 5_000, +}; + + // 에러 메시지 정리 export const ERROR_MESSAGES = { // 구입 금액 관련 에러 diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 435bbfde3..7db922c1f 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,14 +1,18 @@ import InputView from '../view/InputView.js'; import OutputView from '../view/OutputView.js'; import LottoStore from '../model/LottoStore.js'; -import { LOTTO_CONFIG, ERROR_MESSAGES } from '../LottoConfig.js'; +import { LOTTO_CONFIG, ERROR_MESSAGES } from '../LottoConfig.js'; +import PrizeCalculator from '../model/PrizeCalculator.js'; import Lotto from "../model/Lotto.js" class LottoController { #lottoStore; + #prizeCalculator; + #purchaseAmount; constructor() { this.#lottoStore = new LottoStore(); + this.#prizeCalculator = new PrizeCalculator(); } async getPurchaseAmount() { @@ -17,6 +21,10 @@ class LottoController { const input = await InputView.readPurchaseAmount(); const amount = this.#validateAmount(input); const count = amount / LOTTO_CONFIG.PRICE_PER_TICKET; + + // 구매 금액 저장 + this.#purchaseAmount = amount; + return count; } catch (error) { OutputView.printError(error.message); @@ -136,6 +144,34 @@ class LottoController { return number; } + + /** + 당첨 결과를 계산하고 출력을 요청 + @param {number[]} winningNumbers + @param {number} bonusNumber + */ + calculateAndShowResults(winningNumbers, bonusNumber) { + // 1. 모델(LottoStore)에서 로또 목록 가져오기 + const lottos = this.#lottoStore.getLottos(); + + // 2. 모델(PrizeCalculator)에 계산 요청 + const results = this.#prizeCalculator.calculateResults( + lottos, + winningNumbers, + bonusNumber + ); + + const totalPrize = this.#prizeCalculator.calculateTotalPrize(results); + + const rateOfReturn = this.#prizeCalculator.calculateRateOfReturn( + totalPrize, + this.#purchaseAmount // 컨트롤러에 저장된 구매 금액 사용 + ); + + // 3. 뷰(OutputView)에 출력 요청 + OutputView.printResults(results, rateOfReturn); + } } + export default LottoController; diff --git a/src/model/PrizeCalculator.js b/src/model/PrizeCalculator.js index e69de29bb..5bc0d32e3 100644 --- a/src/model/PrizeCalculator.js +++ b/src/model/PrizeCalculator.js @@ -0,0 +1,79 @@ +import { RANK, PRIZE_MONEY } from '../LottoConfig.js'; + +//당첨 결과 계산 및 수익률 계산을 담당 +class PrizeCalculator { + /** + 로또 1장의 당첨 등수를 판별 + @param {Lotto} lotto - 검사할 로또 객체 + @param {number[]} winningNumbers - 당첨 번호 + @param {number} bonusNumber - 보너스 번호 + @returns {string | null} - 당첨 등수 (RANK[key] 또는 null) + */ + #determineRank(lotto, winningNumbers, bonusNumber) { + const matchCount = lotto.countMatch(winningNumbers); + const hasBonus = lotto.hasBonus(bonusNumber); + + if (matchCount === 6) return RANK.FIRST; + if (matchCount === 5 && hasBonus) return RANK.SECOND; + if (matchCount === 5) return RANK.THIRD; + if (matchCount === 4) return RANK.FOURTH; + if (matchCount === 3) return RANK.FIFTH; + return null; + } + + /** + * 구매한 모든 로또의 당첨 결과를 집계 + * @param {Lotto[]} lottos - 구매한 로또 목록 + * @param {number[]} winningNumbers + * @param {number} bonusNumber + * @returns {Map} - 등수별 당첨 횟수 (e.g., Map{FIFTH: 1, ...}) + */ + calculateResults(lottos, winningNumbers, bonusNumber) { + // Map을 사용해 5등 -> 1등 순서(출력 순서)를 보장 + const results = new Map([ + [RANK.FIFTH, 0], + [RANK.FOURTH, 0], + [RANK.THIRD, 0], + [RANK.SECOND, 0], + [RANK.FIRST, 0], + ]); + + lottos.forEach((lotto) => { + const rank = this.#determineRank(lotto, winningNumbers, bonusNumber); + if (rank) { + results.set(rank, results.get(rank) + 1); + } + }); + + return results; + } + + /** + * 당첨 통계를 기반으로 총 상금 계산 + * @param {Map} results - 당첨 통계 + * @returns {number} - 총 상금 + */ + calculateTotalPrize(results) { + let totalPrize = 0; + results.forEach((count, rank) => { + totalPrize += (PRIZE_MONEY[rank] || 0) * count; + }); + return totalPrize; + } + + /** + * 총 상금과 구매 금액으로 수익률 계산 + * (소수점 둘째 자리에서 반올림) + * @param {number} totalPrize - 총 상금 + * @param {number} purchaseAmount - 총 구매 금액 + * @returns {number} - 수익률 (e.g., 62.5) + */ + calculateRateOfReturn(totalPrize, purchaseAmount) { + if (purchaseAmount === 0) return 0; + const rate = (totalPrize / purchaseAmount) * 100; + // 소수점 둘째 자리에서 반올림하여 첫째 자리까지 표시 + return Math.round(rate * 10) / 10; + } +} + +export default PrizeCalculator; diff --git a/src/view/OutputView.js b/src/view/OutputView.js index 2661b7d08..375d676db 100644 --- a/src/view/OutputView.js +++ b/src/view/OutputView.js @@ -1,13 +1,21 @@ import { Console } from '@woowacourse/mission-utils'; +import { RANK, PRIZE_MONEY } from '../LottoConfig.js'; + + +//출력 메시지 포맷 정의 +const PRIZE_FORMATTER = new Map([ + [RANK.FIFTH, `3개 일치 (${PRIZE_MONEY.FIFTH.toLocaleString()}원)`], + [RANK.FOURTH, `4개 일치 (${PRIZE_MONEY.FOURTH.toLocaleString()}원)`], + [RANK.THIRD, `5개 일치 (${PRIZE_MONEY.THIRD.toLocaleString()}원)`], + [RANK.SECOND, `5개 일치, 보너스 볼 일치 (${PRIZE_MONEY.SECOND.toLocaleString()}원)`], + [RANK.FIRST, `6개 일치 (${PRIZE_MONEY.FIRST.toLocaleString()}원)`], +]); const OutputView = { printPurchaseResult(count) { Console.print(`\n${count}개를 구매했습니다.`); }, - /** - @param {Lotto[]} lottos - */ printLottos(lottos) { lottos.forEach((lotto) => { const numbers = lotto.getNumbers(); @@ -15,10 +23,28 @@ const OutputView = { }); }, + /** + 당첨 통계 및 수익률 출력 + @param {Map} results - 등수별 당첨 횟수 (Calculator에서 생성) + @param {number} rateOfReturn - 계산된 수익률 + */ + printResults(results, rateOfReturn) { + Console.print('\n당첨 통계'); + Console.print('---'); + + // PRIZE_FORMATTER의 순서(5등->1등)대로 출력 + PRIZE_FORMATTER.forEach((message, rank) => { + const count = results.get(rank) || 0; + Console.print(`${message} - ${count}개`); + }); + + // 요구사항: 소수점 둘째 자리에서 반올림 (ex 62.5%) + Console.print(`총 수익률은 ${rateOfReturn.toFixed(1)}%입니다.`); + }, + printError(message) { Console.print(message); }, - }; export default OutputView; \ No newline at end of file From 1681be11b6103acd7ebafe8878c249bf48e95a64 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Mon, 3 Nov 2025 22:56:03 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat(feature/error-handling):=20=EB=8B=B9?= =?UTF-8?q?=EC=B2=A8=20=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=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 - Lotto.js test 케이스를 위한 파일 위치 변경 - PrizeCalculator.test.js 테스트 추가 - 당첨 결과 테스트, 수익율 테스트 --- __tests__/PrizeCalculator.test.js | 55 +++++++++++++++++++++++++++++++ src/{model => }/Lotto.js | 2 +- src/controller/LottoController.js | 5 ++- src/model/LottoStore.js | 2 +- 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 __tests__/PrizeCalculator.test.js rename src/{model => }/Lotto.js (96%) diff --git a/__tests__/PrizeCalculator.test.js b/__tests__/PrizeCalculator.test.js new file mode 100644 index 000000000..714a26253 --- /dev/null +++ b/__tests__/PrizeCalculator.test.js @@ -0,0 +1,55 @@ +import Lotto from '../src/Lotto.js'; +import PrizeCalculator from '../src/model/PrizeCalculator.js'; +import { RANK, LOTTO_CONFIG } from '../src/LottoConfig.js'; + +describe('PrizeCalculator 테스트', () => { + let prizeCalculator; + + beforeEach(() => { + prizeCalculator = new PrizeCalculator(); + }); + + const winningNumbers = [1, 2, 3, 4, 5, 6]; + const bonusNumber = 7; + + // 1. 당첨 결과 (calculateResults) 테스트 + test.each([ + // [설명, 로또번호, 기대 등수] + ['1등 (6개 일치)', new Lotto([1, 2, 3, 4, 5, 6]), RANK.FIRST], + ['2등 (5개 + 보너스)', new Lotto([1, 2, 3, 4, 5, 7]), RANK.SECOND], + ['3등 (5개 일치)', new Lotto([1, 2, 3, 4, 5, 8]), RANK.THIRD], + ['4등 (4개 일치)', new Lotto([1, 2, 3, 4, 8, 9]), RANK.FOURTH], + ['5등 (3개 일치)', new Lotto([1, 2, 3, 8, 9, 10]), RANK.FIFTH], + ['낙첨 (2개 일치)', new Lotto([1, 2, 8, 9, 10, 11]), null], + ])('%s 테스트', (desc, lotto, expectedRank) => { + const lottos = [lotto]; + const results = prizeCalculator.calculateResults(lottos, winningNumbers, bonusNumber); + + // 기대 등수가 null이 아니면, 해당 등수가 1개여야 함 + if (expectedRank) { + expect(results.get(expectedRank)).toBe(1); + } + + // 5등부터 1등까지 총합이 1 또는 0 (낙첨) 이어야 함 + const totalWins = Array.from(results.values()).reduce((a, b) => a + b, 0); + expect(totalWins).toBe(expectedRank ? 1 : 0); + }); + + // 2. 수익률 (calculateRateOfReturn) 테스트 + test('총 수익률을 소수점 둘째 자리에서 반올림하여 계산한다 (예: 62.5%)', () => { + // 8000원 구매, 5000원(5등) 당첨 + const purchaseAmount = 8000; + const totalPrize = 5000; + const rate = prizeCalculator.calculateRateOfReturn(totalPrize, purchaseAmount); + + // (5000 / 8000) * 100 = 62.5 + expect(rate).toBe(62.5); + }); + + test('수익률 계산 시 100.0%인 경우', () => { + const purchaseAmount = 1000; + const totalPrize = 1000; + const rate = prizeCalculator.calculateRateOfReturn(totalPrize, purchaseAmount); + expect(rate).toBe(100.0); + }); +}); diff --git a/src/model/Lotto.js b/src/Lotto.js similarity index 96% rename from src/model/Lotto.js rename to src/Lotto.js index f19865d60..eb48acd18 100644 --- a/src/model/Lotto.js +++ b/src/Lotto.js @@ -1,4 +1,4 @@ -import { LOTTO_CONFIG, ERROR_MESSAGES } from '../LottoConfig.js'; +import { LOTTO_CONFIG, ERROR_MESSAGES } from './LottoConfig.js'; class Lotto { #numbers; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 7db922c1f..0fa311de7 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -3,7 +3,7 @@ import OutputView from '../view/OutputView.js'; import LottoStore from '../model/LottoStore.js'; import { LOTTO_CONFIG, ERROR_MESSAGES } from '../LottoConfig.js'; import PrizeCalculator from '../model/PrizeCalculator.js'; -import Lotto from "../model/Lotto.js" +import Lotto from "../Lotto.js" class LottoController { #lottoStore; @@ -58,6 +58,9 @@ class LottoController { // 2. 모델에서 생성된 로또 목록 가져오기 const lottos = this.#lottoStore.getLottos(); + // test에서 lotto 몇개 구매했는지 뜨게 함 + OutputView.printPurchaseResult(count); + // 3. 뷰(OutputView)에 출력 요청 OutputView.printLottos(lottos); } diff --git a/src/model/LottoStore.js b/src/model/LottoStore.js index 730ce517d..995556c69 100644 --- a/src/model/LottoStore.js +++ b/src/model/LottoStore.js @@ -2,7 +2,7 @@ import { Random } from '@woowacourse/mission-utils'; import { LOTTO_CONFIG } from '../LottoConfig.js'; -import Lotto from './Lotto.js'; +import Lotto from '../Lotto.js'; class LottoStore { #lottos; From 15e1bd74e643749a73838dad9ecae8d532b74bb2 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Mon, 3 Nov 2025 23:10:05 +0900 Subject: [PATCH 8/9] docs(update): docs update --- README.md | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e5aaaa4c4..77d59c1de 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,41 @@ 로또 ## 프로젝트 개요 +콘솔(Console) 환경에서 동작하는 로또 게임 애플리케이션입니다. 사용자가 구입 금액을 입력하면 로또를 발급하고, +당첨 번호와 보너스 번호를 입력받아 당첨 통계 및 수익률을 계산합니다. 사용자는 구매 금액을 입력하면 자동으로 로또 번호가 발행됩니다. +이후 당첨 번호와 보너스 번호를 입력하면, 전체 당첨 통계와 총 수익률이 계산되어 출력됩니다. -- 목표: 자세하게 설명 예정 +- 목표: 로또 발매기 및 여러 에러 케이스 정리 - 패턴: MVC (Model-View-Controller) -- 프로젝트 개요 및 도전할 테스트 케이스 정리 -- 기능적 요소 정리 +## 기능적 요소 +| 기능 | 설명 | +| ------------------------ | ----------------------------------- | +| **구매 금액 입력 및 유효성 검사** | 1,000원 단위 금액만 허용. 0 이하 금액 예외 처리 | +| **로또 자동 발행** | 1~45 사이의 숫자 6개 랜덤 생성 (중복 없음) | +| **당첨 결과 계산** | 3개 이상 일치 시 당첨. 5개+보너스 시 2등 처리 | +| **통계 및 수익률 계산** | 전체 구매 대비 수익률 (%) 계산 | +| **에러 및 입력 검증 시스템**| 통합 예외 처리 | +| **자동 시뮬레이션** | 1만 장 구매 등 대규모 통계 테스트 가능 | + ## 기능 요구 사항 +주요 설계 결정 +1. MVC (Model-View-Controller) 패턴 적용 +- Model (Lotto, LottoStore, PrizeCalculator): 데이터와 비즈니스 로직을 담당합니다. +- View (InputView, OutputView): 콘솔 입출력(UI)을 담당하며, 로직을 가지지 않습니다. +- Controller (LottoController): View로부터 입력을 받아 Model을 제어하고, Model의 데이터를 View를 통해 출력합니다. +- 이를 통해 **관심사 분리(Separation of Concerns)**를 달성하여 코드의 유지보수성과 테스트 용이성을 높였습니다. -- 기술 예정 +2. 도메인 객체의 자가 유효성 검증 (Self-Validation) +- Lotto 클래스는 생성자(constructor)에서 로또 번호의 개수(6개), 중복, 숫자 범위(1~45)를 스스로 검증합니다. +- 따라서 LottoController는 당첨 번호를 입력받을 때도 new Lotto(numbers)를 호출하는 것만으로 검증 로직을 재사용 가능. -## 아키텍쳐 +3. 상수 설정 +- LottoConfig.js 파일에 로또 가격, 번호 범위, 당첨금, 모든 에러 메시지를 상수로 정의 +- 로또 가격 변경하거나 "에러 메시지 수정"이 필요할 때, 이 파일 한 곳만 수정하면 전체 반영. +## 아키텍쳐 ``` MVC 패턴 📁 src @@ -33,11 +55,7 @@ MVC 패턴 └── LottoConfig.js # 번호 범위, 가격, 상금, 등수 설정 ``` -- Controller: InputView → Model → OutputView 연결, 에러 처리 및 흐름 제어 -- Model: Lotto, LottoStore, PrizeCalculator (SRP 준수) -- View: InputView / OutputView → 단위 테스트용 Mock 가능 -- Constants: 재사용 가능한 설정 관리 - +## branch 구조 | 브랜치 이름 | 담당 기능 | 상세 내용 | 테스트 포인트 | | ---------------------------- | ------------------ | --------------------------------------------------------- | --------------------------------------------- | | `feature/set-up` | 기본 구조 생성 | MVC 패턴 별 폴더, 파일 생성 | 기본적인 프로젝트 생성 | @@ -46,7 +64,6 @@ MVC 패턴 | `feature/winning-input` | 당첨 번호 입력 | 쉼표 구분 6개 숫자 입력, 범위 1~45, 중복 불가 | 번호 개수, 범위, 중복, 재입력 흐름 | | `feature/result-calculation` | 당첨 결과 & 수익률 | 등수 판정, 당첨 개수 계산, 총 수익률 계산 | 등수 판정 로직, 보너스 번호 판정, 수익률 계산 | | `feature/error-handling` | 예외 처리 | 금액, 번호, 보너스 입력 오류 처리, `[ERROR]` 메시지 통일 | Error 메시지 테스트, 재입력 흐름 검증 | -| `feature/io-abstraction` | 입출력 추상화 | InputView / OutputView, Console 직접 호출 제거, Mock 가능 | Input/Output Mock 테스트, 출력 포맷 확인 | ## 코드적 요소 | 요소 | 상세 내용 | @@ -57,4 +74,6 @@ MVC 패턴 | 입출력 추상화 | InputView / OutputView 인터페이스 적용 → Mocking 가능 | | 설정화 | LottoConfig → 번호 범위, 가격, 상금, 등수 관리 | -요구사항: SRP, 함수 길이 15줄 이하, 3항 연산자 사용 금지, 함수형 프로그래밍 일부 적용 \ No newline at end of file +요구사항: SRP, 함수 길이 15줄 이하, 3항 연산자 사용 금지, 함수형 프로그래밍 일부 적용 + +👤 개발자 이름: 이원형 프리코스 과제: 자동차 경주 (racingcar-precourse) \ No newline at end of file From 9e521d995cc83d75a18db8059d790c0b58c5cf71 Mon Sep 17 00:00:00 2001 From: kpss0337 Date: Mon, 3 Nov 2025 23:20:44 +0900 Subject: [PATCH 9/9] feat(add): add test LottoSimulation.test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1만장 구매 자동화 테스트 --- __tests__/LottoSimulation.test.js | 110 ++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 __tests__/LottoSimulation.test.js diff --git a/__tests__/LottoSimulation.test.js b/__tests__/LottoSimulation.test.js new file mode 100644 index 000000000..63b7d1fde --- /dev/null +++ b/__tests__/LottoSimulation.test.js @@ -0,0 +1,110 @@ +import LottoStore from '../src/model/LottoStore.js'; +import PrizeCalculator from '../src/model/PrizeCalculator.js'; +import OutputView from '../src/view/OutputView.js'; +import Lotto from '../src/Lotto.js'; +import { LOTTO_CONFIG } from '../src/LottoConfig.js'; +import { Random, Console } from '@woowacourse/mission-utils'; + +// Console만 모킹하고, Random은 실제 구현을 사용하도록 설정 +jest.mock('@woowacourse/mission-utils', () => ({ + Random: jest.requireActual('@woowacourse/mission-utils').Random, + // Console.print만 console.log로 연결하여 출력이 보이게 + Console: { + print: jest.fn(console.log), + readLineAsync: jest.fn(), + }, +})); + +describe('🧪 Lotto Simulation (1만 장 통계 테스트)', () => { + // --- 3. 기존 simulation.js의 헬퍼 함수들을 테스트 스위트 내부에 정의 --- + + /** + 당첨 번호 6개를 무작위로 생성하고 정렬 + @returns {number[]} - 정렬된 당첨 번호 + */ + const generateWinningNumbers = () => { + const numbers = Random.pickUniqueNumbersInRange( + LOTTO_CONFIG.MIN_NUMBER, + LOTTO_CONFIG.MAX_NUMBER, + LOTTO_CONFIG.NUMBER_COUNT, + ); + // Lotto 모델 번호 정렬 + const lotto = new Lotto(numbers); + return lotto.getNumbers(); + }; + + /** + 당첨 번호와 겹치지 않는 보너스 번호 1개를 무작위로 생성 + @param {number[]} winningNumbers - 당첨 번호 배열 + @returns {number} - 보너스 번호 + */ + const generateBonusNumber = (winningNumbers) => { + while (true) { + const number = Random.pickNumberInRange( + LOTTO_CONFIG.MIN_NUMBER, + LOTTO_CONFIG.MAX_NUMBER, + ); + if (!winningNumbers.includes(number)) { + return number; + } + } + }; + + const printSimulationHeader = (count, amount, winning, bonus) => { + Console.print('--- 🧪 자동 시뮬레이션 결과 ---'); + Console.print(`[시뮬레이션 조건]`); + Console.print(`- 구매 개수: ${count.toLocaleString()}개`); + Console.print(`- 총 구매액: ${amount.toLocaleString()}원`); + Console.print(`- (자동 생성) 당첨 번호: [${winning.join(', ')}]`); + Console.print(`- (자동 생성) 보너스 번호: ${bonus}`); + }; + + // 각 테스트 전에 print 호출 기록 초기화 + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('1만 장 구매 시뮬레이션이 실행되고 통계가 콘솔에 출력되어야 한다.', () => { + // 시뮬레이션 로직을 테스트 케이스 내에서 직접 실행 + + // 시뮬레이션 설정 + const SIMULATION_COUNT = 10_000; + const PURCHASE_AMOUNT = SIMULATION_COUNT * LOTTO_CONFIG.PRICE_PER_TICKET; + + // 1. 로또 대량 구매 + const lottoStore = new LottoStore(); + lottoStore.generateLottos(SIMULATION_COUNT); + const lottos = lottoStore.getLottos(); + + // 2. 당첨/보너스 번호 생성 + const winningNumbers = generateWinningNumbers(); + const bonusNumber = generateBonusNumber(winningNumbers); + + // 3. 당첨 결과 계산 + const prizeCalculator = new PrizeCalculator(); + const results = prizeCalculator.calculateResults( + lottos, + winningNumbers, + bonusNumber, + ); + const totalPrize = prizeCalculator.calculateTotalPrize(results); + const rateOfReturn = prizeCalculator.calculateRateOfReturn( + totalPrize, + PURCHASE_AMOUNT, + ); + + // 4. 시뮬레이션 결과 출력 + printSimulationHeader( + SIMULATION_COUNT, + PURCHASE_AMOUNT, + winningNumbers, + bonusNumber, + ); + OutputView.printResults(results, rateOfReturn); + + // 5. 테스트 검증 + expect(Console.print).toHaveBeenCalled(); + const lastCall = Console.print.mock.calls[Console.print.mock.calls.length - 1]; + expect(lastCall[0]).toEqual(expect.stringContaining('총 수익률은')); + }); +}); \ No newline at end of file