diff --git a/README.md b/README.md index 15bb106b5..1f2427ce4 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ # javascript-lotto-precourse +1. 로또 구입 금액을 입력받는다. (Console API 사용) +2. 구입 금액이 1,000원 단위인지 검증한다. (예외 처리) +3. 당첨 번호 6개를 입력받는다. (쉼표 구분) +4. 당첨 번호의 유효성을 검증한다. (1~45 범위, 중복, 개수 등) +5. 보너스 번호 1개를 입력받는다. +6. 보너스 번호의 유효성을 검증한다. (1~45 범위, 당첨 번호와 중복 등) +7. 구입 금액에 맞춰 로또를 발행한다. (1장=1000원) +8. 로또 번호는 Random.pickUniqueNumbersInRange를 사용하여 6개를 뽑는다. +9. 발행된 로또 번호를 오름차순으로 정렬하여 출력한다. +10. 구매 로또별로 당첨 번호와 비교하여 일치 개수 및 보너스 번호 일치 여부를 계산한다. +11. 계산 결과에 따라 5등부터 1등까지 당첨 내역을 집계한다. +12. 당첨 내역(매칭 개수) 및 금액을 출력 요구사항 형식에 맞춰 출력한다. +13. 총 상금과 구입 금액을 이용하여 수익률을 계산한다. +14. 수익률을 소수점 둘째 자리에서 반올림하여 출력한다. (예: 62.5%) diff --git a/src/App.js b/src/App.js index 091aa0a5d..9b620fffd 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,36 @@ +import { Console } from "@woowacourse/mission-utils"; +import LottoGame from "./LottoGame.js"; + class App { - async run() {} + async run() { + const game = new LottoGame(); + + try { + // 1. 구입 금액 입력 및 로또 발행 + await game.getPurchaseAmount(); + game.issueLottos(); + + // 2. 당첨 번호 입력 및 검증 (LottoGame에 구현된 반복 입력 처리) + const winningLottoInstance = await game.getWinningNumbers(); + const winningNumbers = winningLottoInstance.getNumbers(); + + // 3. 보너스 번호 입력 및 검증 (LottoGame에 구현된 반복 입력 처리) + // LotteGame에 추가한 헬퍼 메서드를 통해 유효한 번호를 받습니다. + const bonusNumber = await game.getBonusNumberValue(); + + // 4. WinningLotto 객체 생성 (최종 중복 검사) + // LottoGame.js의 calculateAndPrintResults에서 WinningLotto 객체를 생성합니다. + + // 5. 당첨 결과 계산 및 출력 (10~14번 기능) + game.calculateAndPrintResults(winningNumbers, bonusNumber); + + } catch (error) { + // 1.6번 기능: [ERROR] 메시지 출력 후 애플리케이션 종료 + Console.print(error.message); + // 테스트 통과 및 비정상 종료 상태를 명확히 하기 위해 throw error를 유지합니다. + throw error; + } + } } -export default App; +export default App; \ No newline at end of file diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..2e8e209d0 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -4,15 +4,43 @@ class Lotto { constructor(numbers) { this.#validate(numbers); this.#numbers = numbers; + this.#numbers.sort((a, b) => a - b); // 9. 번호 정렬 (미리 해둠) } + // 기존 #validate에 유효성 검사를 추가합니다. #validate(numbers) { if (numbers.length !== 6) { throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } + // 4. 유효성 검증: 1~45 범위 및 중복 검사 + if (numbers.some(number => number < 1 || number > 45)) { + throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + if (new Set(numbers).size !== 6) { + throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + } + + getNumbers() { + return this.#numbers; + } + + // 10번 기능: 당첨 번호와의 일치 개수 및 보너스 번호 일치 여부 계산 + getMatchResult(winningLotto, bonusNumber) { + const winningNumbers = winningLotto.getNumbers(); + + // 일치 개수 계산 + const matchCount = this.#numbers.filter(number => + winningNumbers.includes(number) + ).length; + + // 보너스 번호 일치 여부 + const bonusMatch = this.#numbers.includes(bonusNumber); + + return { matchCount, bonusMatch }; } - // TODO: 추가 기능 구현 + // ... (나중에 추가할예정) } export default Lotto; diff --git a/src/LottoGame.js b/src/LottoGame.js new file mode 100644 index 000000000..5ac770d66 --- /dev/null +++ b/src/LottoGame.js @@ -0,0 +1,129 @@ +import { Console, Random } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; +// PRIZES 상수를 사용하기 위해 WinningLotto에서 import해야 합니다. +import WinningLotto, { PRIZES } from "./WinningLotto.js"; + +const LOTTO_PRICE = 1000; + +// 당첨 내역 초기화 및 출력 순서를 정의합니다. (파일 상단에 위치) +const INITIAL_STATS = { + [PRIZES.FIFTH.label]: 0, + [PRIZES.FOURTH.label]: 0, + [PRIZES.THIRD.label]: 0, + [PRIZES.SECOND.label]: 0, + [PRIZES.FIRST.label]: 0, +}; + +// ✨ 클래스는 파일당 한 번만 정의됩니다. +class LottoGame { + constructor() { + this.lottos = []; + this.purchaseAmount = 0; + } + + // 1, 2번 기능: 구입 금액 입력 및 검증 로직 (반복 입력 처리 포함) + // 3, 4번 기능: 당첨 번호 입력 및 검증 (반복 처리) + // 5번 기능: 보너스 번호 입력 및 기본 유효성 검증 (반복 처리) + async getBonusNumberValue() { + while (true) { + try { + const input = await Console.readLineAsync("\n보너스 번호를 입력해 주세요.\n"); + const number = parseInt(input.trim(), 10); + + // 기본 유효성 검증 (숫자형, 1~45 범위) + if (isNaN(number) || number < 1 || number > 45) { + // WinningLotto.js의 에러 메시지를 재사용하거나 적절한 에러를 던집니다. + throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + return number; + } catch (error) { + Console.print(error.message); + } + } + } + + // 5, 6번 기능: 보너스 번호 입력 및 검증 (반복 처리) + async getBonusNumber(winningLotto) { + while (true) { + try { + const input = await Console.readLineAsync("\n보너스 번호를 입력해 주세요.\n"); + const number = parseInt(input.trim(), 10); + + // 6. 유효성 검증: WinningLotto 생성자를 통해 검증이 실행됨 + return new WinningLotto(winningLotto.getNumbers(), number); + } catch (error) { + Console.print(error.message); + } + } + } + + // 2번 기능: 금액 유효성 검증 로직 + validateAmount(input) { + const amount = parseInt(input.trim(), 10); + if (isNaN(amount)) { + throw new Error("[ERROR] 구입 금액은 숫자여야 합니다."); + } + if (amount % LOTTO_PRICE !== 0 || amount === 0) { + throw new Error("[ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다."); + } + return amount; + } + + // 7, 8, 9번 기능: 로또 발행 로직 + issueLottos() { + const count = this.purchaseAmount / LOTTO_PRICE; + Console.print(`\n${count}개를 구매했습니다.`); + + for (let i = 0; i < count; i++) { + const numbers = Random.pickUniqueNumbersInRange(1, 45, 6); + this.lottos.push(new Lotto(numbers)); + Console.print(`[${this.lottos[i].getNumbers().join(', ')}]`); + } + } + + // 12, 13번 기능: 당첨 통계 계산 및 출력 + calculateAndPrintResults(winningNumbers, bonusNumber) { + // winningLotto 인스턴스 생성 시 유효성 검사가 WinningLotto 생성자에서 실행됩니다. + const winningLotto = new WinningLotto(winningNumbers, bonusNumber); + + const stats = this.lottos.reduce((acc, lotto) => { + const rank = winningLotto.rank(lotto); + if (rank) { + acc[rank.label] += 1; + } + return acc; + }, { ...INITIAL_STATS }); + + this.printWinningStats(stats); + this.printProfitRate(stats); + } + + // 12번 기능: 당첨 내역 출력 + printWinningStats(stats) { + Console.print('\n당첨 통계'); + Console.print('---'); + Object.keys(INITIAL_STATS).forEach(label => { + Console.print(`${label} - ${stats[label]}개`); + }); + } + + // 14번 기능: 수익률 계산 및 출력 + printProfitRate(stats) { + let totalRevenue = 0; + Object.keys(stats).forEach(label => { + // PRIZES 상수는 WinningLotto에서 export 했으므로 여기에서 직접 접근 가능 + const prize = Object.values(PRIZES).find(p => p.label === label); + if (prize) { // prize가 존재하는지 확인 (안전성 추가) + totalRevenue += stats[label] * prize.amount; + } + }); + + const profitRate = (totalRevenue / this.purchaseAmount) * 100; + + // 수익률 소수점 둘째 자리에서 반올림 + const roundedRate = Math.round(profitRate * 10) / 10; + Console.print(`총 수익률은 ${roundedRate.toLocaleString('ko-KR')}%입니다.`); + } +} + +export default LottoGame; \ No newline at end of file diff --git a/src/WinningLotto.js b/src/WinningLotto.js new file mode 100644 index 000000000..fd401ba4f --- /dev/null +++ b/src/WinningLotto.js @@ -0,0 +1,50 @@ +import Lotto from "./Lotto.js"; + +// 1. PRIZES 상수를 파일 맨 위에 정의하고 export 합니다. +export const PRIZES = { + FIRST: { count: 6, bonus: false, amount: 2000000000, label: '6개 일치 (2,000,000,000원)' }, + SECOND: { count: 5, bonus: true, amount: 30000000, label: '5개 일치, 보너스 볼 일치 (30,000,000원)' }, + THIRD: { count: 5, bonus: false, amount: 1500000, label: '5개 일치 (1,500,000원)' }, + FOURTH: { count: 4, bonus: false, amount: 50000, label: '4개 일치 (50,000원)' }, + FIFTH: { count: 3, bonus: false, amount: 5000, label: '3개 일치 (5,000원)' }, +}; + +class WinningLotto { + constructor(winningNumbers, bonusNumber) { + this.winningLotto = new Lotto(winningNumbers); + this.bonusNumber = this.validateBonusNumber(bonusNumber); + } + + // 6번 기능: 보너스 번호 유효성 검증 로직 + validateBonusNumber(number) { + const num = parseInt(number, 10); + + if (isNaN(num)) { + throw new Error("[ERROR] 보너스 번호는 숫자여야 합니다."); + } + if (num < 1 || num > 45) { + throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + if (this.winningLotto.getNumbers().includes(num)) { + throw new Error("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + + return num; + } + + // 11번 기능: 로또 번호와 비교하여 당첨 등수를 반환 + rank(lotto) { + const { matchCount, bonusMatch } = lotto.getMatchResult(this.winningLotto, this.bonusNumber); + + // 1등부터 5등까지 순차적으로 조건 검사 + if (matchCount === 6) return PRIZES.FIRST; + if (matchCount === 5 && bonusMatch) return PRIZES.SECOND; + if (matchCount === 5) return PRIZES.THIRD; + if (matchCount === 4) return PRIZES.FOURTH; + if (matchCount === 3) return PRIZES.FIFTH; + + return null; // 낙첨 + } +} + +export default WinningLotto; \ No newline at end of file