Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[로또] 김진우 미션 제출합니다. #400

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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] 구현한 기능에 대한 단위 테스트를 작성하였는가?
19 changes: 17 additions & 2 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
19 changes: 19 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
});
});
90 changes: 89 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -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;
42 changes: 42 additions & 0 deletions src/Calculator.js
Original file line number Diff line number Diff line change
@@ -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;
21 changes: 21 additions & 0 deletions src/InputHandler.js
Original file line number Diff line number Diff line change
@@ -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;
15 changes: 15 additions & 0 deletions src/Lotto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
31 changes: 31 additions & 0 deletions src/Printer.js
Original file line number Diff line number Diff line change
@@ -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;