diff --git a/README.md b/README.md index 15bb106b5..fb5f05946 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ # javascript-lotto-precourse +# 🏫 μš°μ•„ν•œν…Œν¬μ½”μŠ€ 7κΈ° ν”„λ¦¬μ½”μŠ€ 3μ£Όμ°¨ λ―Έμ…˜: 둜또 발맀기 +## ν•™μŠ΅ λͺ©ν‘œ +- κ΄€λ ¨ ν•¨μˆ˜λ₯Ό λ¬Άμ–΄ 클래슀λ₯Ό λ§Œλ“€κ³ , 객체듀이 ν˜‘λ ₯ν•˜μ—¬ ν•˜λ‚˜μ˜ 큰 κΈ°λŠ₯을 μˆ˜ν–‰ν•˜λ„λ‘ ν•œλ‹€. +- ν΄λž˜μŠ€μ™€ ν•¨μˆ˜μ— λŒ€ν•œ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό 톡해 μ˜λ„ν•œ λŒ€λ‘œ μ •ν™•ν•˜κ²Œ μž‘λ™ν•˜λŠ” μ˜μ—­μ„ ν™•λ³΄ν•œλ‹€. +- 2μ£Ό μ°¨ 곡톡 ν”Όλ“œλ°±μ„ μ΅œλŒ€ν•œ λ°˜μ˜ν•œλ‹€. + +## πŸ“ μš”κ΅¬μ‚¬ν•­λΆ„μ„ +### μž…λ ₯ +- [x] 둜또 κ΅¬μž… κΈˆμ•‘ μž…λ ₯ λ°›κΈ° +- [x] 둜또 당첨 번호 μž…λ ₯ λ°›κΈ° +- [x] λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ λ°›κΈ° + +### 좜λ ₯ +- [x] 당첨 톡계 좜λ ₯ +- [x] 총 수읡λ₯  좜λ ₯ + +### 둜또 +- [x] λžœλ€ν•œ 6개의 숫자 λ°œν–‰ +- [x] 둜또 당첨 λ²ˆν˜Έμ™€ κ΅¬λ§€ν•œ 둜또 번호 λΉ„κ΅ν•˜μ—¬ 일치 개수 μ‚°μΆœ +- [x] λ³΄λ„ˆμŠ€ λ²ˆν˜Έμ™€ 둜또 번호 일치 μ—¬λΆ€ μ €μž₯ +- [x] ꡬ맀 κΈˆμ•‘κ³Ό 총 당첨 κΈˆμ•‘μ„ λΉ„κ΅ν•˜μ—¬ 수읡λ₯  계산 + +## ⚠️ μ˜ˆμ™Έμ²˜λ¦¬ +### μž…λ ₯ μ˜ˆμ™Έ 처리 +## μž…λ ₯ μ˜ˆμ™Έ 처리 +- [x] κ΅¬μž… κΈˆμ•‘μ΄ 1000의 λ°°μˆ˜κ°€ 아닐 경우: κ΅¬μž… κΈˆμ•‘μ€ 1000원 λ‹¨μœ„μ—¬μ•Ό ν•˜λ©°, 1000으둜 λ‚˜λˆ„μ–΄ 떨어지지 μ•ŠμœΌλ©΄ μ˜ˆμ™Έ λ°œμƒ. +- [x] κ΅¬μž… κΈˆμ•‘μ΄ μˆ«μžκ°€ 아닐 경우: κ΅¬μž… κΈˆμ•‘ μž…λ ₯ μ‹œ 숫자만 ν—ˆμš©λ˜λ©°, λ¬Έμžμ—΄ λ“± μˆ«μžκ°€ μ•„λ‹Œ 값이 μž…λ ₯되면 μ˜ˆμ™Έ λ°œμƒ. +### 둜또 번호 μž…λ ₯ μ˜ˆμ™Έ 처리 +- [x] 둜또 번호의 κ°œμˆ˜κ°€ 6κ°œκ°€ 아닐 경우: 둜또 λ²ˆν˜ΈλŠ” λ°˜λ“œμ‹œ 6κ°œμ—¬μ•Ό ν•˜λ©°, κ·Έ 이상 λ˜λŠ” μ΄ν•˜μΌ 경우 μ˜ˆμ™Έ λ°œμƒ. +- [x] 둜또 λ²ˆν˜Έμ— μ€‘λ³΅λœ μˆ«μžκ°€ μžˆμ„ 경우: 각 둜또 λ²ˆν˜ΈλŠ” μ€‘λ³΅λ˜μ§€ μ•Šμ•„μ•Ό ν•˜λ©°, μ€‘λ³΅λœ λ²ˆν˜Έκ°€ 있으면 μ˜ˆμ™Έ λ°œμƒ. +- [x] 둜또 λ²ˆν˜Έκ°€ 1μ—μ„œ 45 μ‚¬μ΄μ˜ μˆ«μžκ°€ 아닐 경우: λͺ¨λ“  둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 사이에 μžˆμ–΄μ•Ό ν•˜λ©°, λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λŠ” μˆ«μžκ°€ 포함될 경우 μ˜ˆμ™Έ λ°œμƒ. +- [x] 둜또 λ²ˆν˜Έκ°€ μˆ«μžκ°€ μ•„λ‹Œ 경우: 둜또 번호 μž…λ ₯ μ‹œ 숫자만 ν—ˆμš©λ˜λ©°, λ¬Έμžμ—΄ λ“± μˆ«μžκ°€ μ•„λ‹Œ 값이 ν¬ν•¨λ˜λ©΄ μ˜ˆμ™Έ λ°œμƒ. +### λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ μ˜ˆμ™Έ 처리 +- [x] λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 1μ—μ„œ 45 μ‚¬μ΄μ˜ μˆ«μžκ°€ 아닐 경우: λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•˜λ©°, 이 λ²”μœ„λ₯Ό λ²—μ–΄λ‚  경우 μ˜ˆμ™Έ λ°œμƒ. +- [x] λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 당첨 λ²ˆν˜Έμ™€ 쀑볡될 경우: λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜μ§€ μ•Šμ•„μ•Ό ν•˜λ©°, 당첨 λ²ˆν˜Έμ— ν¬ν•¨λœ 경우 μ˜ˆμ™Έ λ°œμƒ. +### 기타 둜또 둜직 μ˜ˆμ™Έ 처리 +- [x] 둜또 λ²ˆν˜Έμ™€ 당첨 번호 비ꡐ: 둜또 λ²ˆν˜Έμ™€ 당첨 번호λ₯Ό λΉ„κ΅ν•˜μ—¬ μΌμΉ˜ν•˜λŠ” 번호 수λ₯Ό μ‚°μΆœν•˜κ³ , λ³΄λ„ˆμŠ€ λ²ˆν˜Έμ™€λ„ 일치 μ—¬λΆ€λ₯Ό 확인. + +## βœ… 체크리슀트 +- [x] 클래슀 섀계와 κ΅¬ν˜„, λ©”μ„œλ“œ 섀계와 κ΅¬ν˜„ 같은 μƒμ„Έν•œ λ‚΄μš©μ€ κΈ°λŠ₯ λͺ©λ‘μ— ν¬ν•¨ν•˜μ§€ μ•Šμ•˜λŠ”κ°€? +- [x] JavaScript Code Conventions 을 μ€€μˆ˜ν•˜μ˜€λŠ”κ°€? +- [x] ν•œ λ©”μ„œλ“œμ— 였직 ν•œ λ‹¨κ³„μ˜ λ“€μ—¬μ“°κΈ°λ§Œ ν—ˆμš©ν–ˆλŠ”κ°€? +- [x] ν•¨μˆ˜(λ˜λŠ” λ©”μ„œλ“œ)의 길이가 15라인은 λ„˜μ–΄κ°€μ§€ μ•Šμ•˜λŠ”κ°€? +- [x] else μ˜ˆμ•½μ–΄λ₯Ό 쓰지 μ•Šμ•˜λŠ”κ°€? +- [x] λͺ¨λ“  μ›μ‹œκ°’κ³Ό λ¬Έμžμ—΄μ„ 포μž₯ν–ˆλŠ”κ°€? +- [x] 3개 μ΄μƒμ˜ μΈμŠ€ν„΄μŠ€ λ³€μˆ˜λ₯Ό 가진 클래슀λ₯Ό κ΅¬ν˜„ν•˜μ§€ μ•Šμ•˜λŠ”κ°€? +- [x] getter/setter 없이 κ΅¬ν˜„ν–ˆλŠ”κ°€? +- [x] λ©”μ†Œλ“œμ˜ 인자 수λ₯Ό μ œν•œν–ˆλŠ”κ°€? +- [x] μ½”λ“œ ν•œ 쀄에 점(.)을 ν•˜λ‚˜λ§Œ ν—ˆμš©ν–ˆλŠ”κ°€? +- [x] λ©”μ†Œλ“œκ°€ ν•œκ°€μ§€ 일만 λ‹΄λ‹Ήν•˜λ„λ‘ κ΅¬ν˜„ν–ˆλŠ”κ°€? +- [x] 클래슀λ₯Ό μž‘κ²Œ μœ μ§€ν•˜κΈ° μœ„ν•΄ λ…Έλ ₯ν–ˆλŠ”κ°€? +- [x] 3ν•­ μ—°μ‚°μžλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ•˜λŠ”κ°€? +- [x] AngularJS Commit Conventions 에 맞좰 Commit Messageλ₯Ό μž‘μ„±ν–ˆλŠ”κ°€? +- [x] 이름을 μΆ•μ•½ν•˜μ§€ μ•Šκ³  μ˜λ„λ₯Ό 잘 λ“œλŸ¬λƒˆλŠ”κ°€? +- [x] Jestλ₯Ό ν™œμš©ν•΄ ν…ŒμŠ€νŠΈ μ½”λ“œλ‘œ μž‘λ™μ„ ν™•μΈν–ˆλŠ”κ°€? +- [x] κ΅¬ν˜„ν•œ κΈ°λŠ₯에 λŒ€ν•œ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜μ˜€λŠ”κ°€? \ No newline at end of file diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 872380c9c..47052aff5 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -91,7 +91,22 @@ describe("둜또 ν…ŒμŠ€νŠΈ", () => { }); }); - test("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ", async () => { - await runException("1000j"); + // κΈˆμ•‘ μž…λ ₯이 1000원 λ‹¨μœ„κ°€ 아닐 경우 μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ + test.each(["500", "1200", "100", "abcd"])( + "κ΅¬μž… κΈˆμ•‘μ΄ 1000의 λ°°μˆ˜κ°€ 아닐 경우 μ˜ˆμ™Έ 처리: %s", + async (input) => { + await runException(input); + } + ); + + // 둜또 번호 μž…λ ₯에 μœ νš¨ν•˜μ§€ μ•Šμ€ ν˜•μ‹μ΄ 포함될 경우 μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ + test.each([ + "1,2,3,4,5", // μˆ«μžκ°€ 6κ°œκ°€ 아닐 경우 + "1,2,3,4,5,46", // μˆ«μžκ°€ 1~45 λ²”μœ„ 밖일 경우 + "1,2,3,4,5,5", // μ€‘λ³΅λœ μˆ«μžκ°€ 포함될 경우 + "a,b,c,d,e,f", // μˆ«μžκ°€ μ•„λ‹Œ λ¬Έμžκ°€ 포함될 경우 + ])("μœ νš¨ν•˜μ§€ μ•Šμ€ 둜또 번호 μž…λ ₯에 λŒ€ν•œ μ˜ˆμ™Έ 처리: %s", async (input) => { + await runException("1000"); + mockQuestions([input]); }); }); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..b90bced2d 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -15,4 +15,23 @@ describe("둜또 클래슀 ν…ŒμŠ€νŠΈ", () => { }); // TODO: μΆ”κ°€ κΈ°λŠ₯ κ΅¬ν˜„μ— λ”°λ₯Έ ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„± + // 둜또 λ²ˆν˜Έκ°€ λ²”μœ„λ₯Ό λ²—μ–΄λ‚  경우 μ˜ˆμ™Έ λ°œμƒ + test("둜또 λ²ˆν˜Έκ°€ 1~45의 λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.", () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + expect(() => { + new Lotto([1, 2, 3, 4, 5, 46]); + }).toThrow("[ERROR]"); + }); + + // 둜또 λ²ˆν˜Έκ°€ μˆ«μžκ°€ μ•„λ‹Œ 경우 μ˜ˆμ™Έ λ°œμƒ + test("둜또 λ²ˆν˜Έκ°€ μˆ«μžκ°€ 아닐 경우 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.", () => { + expect(() => { + new Lotto(["a", 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + expect(() => { + new Lotto([null, 2, 3, 4, 5, 6]); + }).toThrow("[ERROR]"); + }); }); diff --git a/src/App.js b/src/App.js index 091aa0a5d..8dd63070e 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,93 @@ +import { Console, MissionUtils } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; +import InputHandler from "./InputHandler.js"; +import Printer from "./Printer.js"; +import Calculator from "./Calculator.js"; + +// 메인 App 클래슀 class App { - async run() {} + constructor() { + this.inputHandler = new InputHandler(); + this.printer = new Printer(); + this.calculator = new Calculator(); + } + + async run() { + try { + const purchaseMoney = await this.inputHandler.getPurchaseMoney(); + this.validatePurchaseMoney(purchaseMoney); + + // 둜또 ꡬ맀 및 번호 생성 + let lottos = Array.from({ length: purchaseMoney / 1000 }, () => + MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6) + ); + + this.printer.printLottos(lottos); + lottos = lottos.map((lotto) => new Lotto(lotto)); + + const winningNumber = await this.inputHandler.getWinningNumber(); + this.validateLottoNumbers(winningNumber); + + const bonusNumber = await this.inputHandler.getBonusNumber(); + this.validateBonusNumber(bonusNumber, winningNumber); + + // 당첨 κ²°κ³Ό 맀핑 + const matchResults = lottos.map((lotto) => ({ + matches: lotto.confirmMatches(winningNumber), + hasBonus: lotto.confirmBonus(bonusNumber), + })); + + // 당첨 톡계 및 수읡λ₯  좜λ ₯ + const winningStats = this.calculator.calculateWinningStats(matchResults); + this.printer.printWinningStats(winningStats); + + const returnRate = this.calculator.calculateReturnRate( + matchResults, + purchaseMoney + ); + this.printer.printReturnRate(returnRate); + } catch (error) { + Console.print(`[ERROR] ${error.message}`); + } + } + + // κ΅¬μž… κΈˆμ•‘μ΄ 1000의 λ°°μˆ˜μΈμ§€ 확인 + validatePurchaseMoney(money) { + if (isNaN(money) || money % 1000 !== 0) { + throw new Error("κ΅¬μž… κΈˆμ•‘μ€ 1000의 λ°°μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + } + + // 둜또 번호 μœ νš¨μ„± 검사 + validateLottoNumbers(numbers) { + const lottoNumbers = numbers.split(",").map(Number); + + if (lottoNumbers.length !== 6) { + throw new Error("둜또 λ²ˆν˜ΈλŠ” 6개의 μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + if (new Set(lottoNumbers).size !== 6) { + throw new Error("둜또 λ²ˆν˜Έμ—λŠ” μ€‘λ³΅λœ μˆ«μžκ°€ μ—†μ–΄μ•Ό ν•©λ‹ˆλ‹€."); + } + if (!lottoNumbers.every((num) => num >= 1 && num <= 45)) { + throw new Error("둜또 λ²ˆν˜ΈλŠ” 1μ—μ„œ 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + if (lottoNumbers.some(isNaN)) { + throw new Error("둜또 λ²ˆν˜ΈλŠ” 숫자만 μž…λ ₯λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€."); + } + } + + // λ³΄λ„ˆμŠ€ 번호 μœ νš¨μ„± 검사 + validateBonusNumber(bonusNumber, winningNumbers) { + const bonus = Number(bonusNumber); + const winningSet = new Set(winningNumbers.split(",").map(Number)); + + if (isNaN(bonus) || bonus < 1 || bonus > 45) { + throw new Error("λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1μ—μ„œ 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + if (winningSet.has(bonus)) { + throw new Error("λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜μ§€ μ•Šμ•„μ•Ό ν•©λ‹ˆλ‹€."); + } + } } export default App; diff --git a/src/Calculator.js b/src/Calculator.js new file mode 100644 index 000000000..64c782b73 --- /dev/null +++ b/src/Calculator.js @@ -0,0 +1,42 @@ +// 톡계 및 수읡λ₯  계산 클래슀 +class Calculator { + constructor() { + // 당첨 κΈˆμ•‘ μ„€μ • + this.PRIZE_MONEY = { + 3: 5000, + 4: 50000, + 5: 1500000, + "5_bonus": 30000000, + 6: 2000000000, + }; + } + + // 당첨 톡계 계산 + calculateWinningStats(results) { + const stats = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, "5_bonus": 0, 6: 0 }; + + results.forEach(({ matches, hasBonus }) => { + if (matches === 5 && hasBonus) { + stats["5_bonus"] += 1; + return; + } + stats[matches] += 1; + }); + + return stats; + } + + // 수읡λ₯  계산 + calculateReturnRate(results, money) { + let totalPrizeMoney = 0; + results.forEach(({ matches }) => { + if (this.PRIZE_MONEY[matches]) { + totalPrizeMoney += this.PRIZE_MONEY[matches]; + } + }); + + return Math.round((totalPrizeMoney / money) * 1000) / 10; + } +} + +export default Calculator; diff --git a/src/InputHandler.js b/src/InputHandler.js new file mode 100644 index 000000000..e700c86b9 --- /dev/null +++ b/src/InputHandler.js @@ -0,0 +1,21 @@ +import { Console } from "@woowacourse/mission-utils"; + +// μ‚¬μš©μž μž…λ ₯을 μ²˜λ¦¬ν•˜λŠ” 클래슀 +class InputHandler { + // κ΅¬μž… κΈˆμ•‘ μž…λ ₯ λ°›κΈ° + async getPurchaseMoney() { + return await Console.readLineAsync("κ΅¬μž…κΈˆμ•‘μ„ μž…λ ₯ν•΄ μ£Όμ„Έμš”.\n"); + } + + // 당첨 번호 μž…λ ₯ λ°›κΈ° + async getWinningNumber() { + return await Console.readLineAsync("\n당첨 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”.\n"); + } + + // λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ λ°›κΈ° + async getBonusNumber() { + return await Console.readLineAsync("\nλ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”.\n"); + } +} + +export default InputHandler; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..299512103 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -10,9 +10,24 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€."); } + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== numbers.length) { + throw new Error("[ERROR] 둜또 λ²ˆν˜Έμ— 쀑볡이 μžˆμŠ΅λ‹ˆλ‹€."); + } + if ( + !numbers.every((num) => typeof num === "number" && num >= 1 && num <= 45) + ) { + throw new Error("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } } // TODO: μΆ”κ°€ κΈ°λŠ₯ κ΅¬ν˜„ + confirmMatches(winNum) { + return this.#numbers.filter((number) => winNum.includes(number)).length; + } + confirmBonus(bonusNum) { + return this.#numbers.includes(bonusNum); + } } export default Lotto; diff --git a/src/Printer.js b/src/Printer.js new file mode 100644 index 000000000..398cf244a --- /dev/null +++ b/src/Printer.js @@ -0,0 +1,31 @@ +import { Console } from "@woowacourse/mission-utils"; + +// 좜λ ₯을 μ²˜λ¦¬ν•˜λŠ” 클래슀 +class Printer { + // 둜또 ꡬ맀 λ‚΄μ—­ 좜λ ₯ + printLottos(lottos) { + Console.print(`\n${lottos.length}개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€.`); + lottos.forEach((lotto) => { + Console.print(`[${lotto.join(", ")}]`); + }); + } + + // 당첨 톡계 좜λ ₯ + printWinningStats(stats) { + Console.print(`\n당첨 톡계\n---`); + Console.print(`3개 일치 (5,000원) - ${stats[3]}개`); + Console.print(`4개 일치 (50,000원) - ${stats[4]}개`); + Console.print(`5개 일치 (1,500,000원) - ${stats[5]}개`); + Console.print( + `5개 일치, λ³΄λ„ˆμŠ€ λ³Ό 일치 (30,000,000원) - ${stats["5_bonus"]}개` + ); + Console.print(`6개 일치 (2,000,000,000원) - ${stats[6]}개`); + } + + // 수읡λ₯  좜λ ₯ + printReturnRate(returnRate) { + Console.print(`총 수읡λ₯ μ€ ${returnRate}%μž…λ‹ˆλ‹€.`); + } +} + +export default Printer;