Skip to content
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
# javascript-lotto-precourse

## 기능 구현
1. 사용자로부터 로또 구입 금액을 입력한다.
금액은 1000원 단위로 입력받으며 1000원으로 나누어 떨어지지 않으면 예외처리
2. 구입 금액에 따른 로또 수량에 맞춰 랜덤으로 로또 번호가 6개 생성된다.
로또 번호의 범위는 1~45이며, 중복되지 않는다.
3. 당첨 번호를 입력받는다. 번호는 쉼표(,)를 기준으로 구분한다.
당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
4. 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 종료한다.
수익률을 계산하는 PrizeCheck 클래스를 만들어 코드 분리를 시도해본다.

## 예외 처리
1. 당첨 번호가 6개가 아닌 경우
2. 로또 번호의 범위가 벗어난 경우
3. 로또 번호가 중복된 경우
26 changes: 20 additions & 6 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const mockQuestions = (inputs) => {

const mockRandoms = (numbers) => {
MissionUtils.Random.pickUniqueNumbersInRange = jest.fn();
numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, MissionUtils.Random.pickUniqueNumbersInRange);
numbers.forEach((number) => {
MissionUtils.Random.pickUniqueNumbersInRange.mockReturnValueOnce(number);
});
};

const getLogSpy = () => {
Expand Down Expand Up @@ -51,7 +51,7 @@ describe("로또 테스트", () => {
// given
const logSpy = getLogSpy();

mockRandoms([
const expectedLottos = [
[8, 21, 23, 41, 42, 43],
[3, 5, 11, 16, 32, 38],
[7, 11, 16, 35, 36, 44],
Expand All @@ -60,7 +60,9 @@ describe("로또 테스트", () => {
[7, 11, 30, 40, 42, 43],
[2, 13, 22, 32, 38, 45],
[1, 3, 5, 14, 22, 45],
]);
];

mockRandoms(expectedLottos);
mockQuestions(["8000", "1,2,3,4,5,6", "7"]);

// when
Expand Down Expand Up @@ -91,7 +93,19 @@ describe("로또 테스트", () => {
});
});

test("예외 테스트", async () => {
test("예외 테스트 - 잘못된 입력", async () => {
await runException("1000j");
});

test("예외 테스트 - 1000원 단위로 입력하지 않음", async () => {
await runException("2000");
});

test("예외 테스트 - 당첨 번호 잘못된 형식", async () => {
await runException("1,2,3,4,5,6,7");
});

test("예외 테스트 - 보너스 번호 잘못된 형식", async () => {
await runException("46");
});
});
28 changes: 26 additions & 2 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,36 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});

// TODO: 테스트가 통과하도록 프로덕션 코드 구현
test("로또 번호의 개수가 6개 미만이면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5]);
}).toThrow("[ERROR]");
});

test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 5]);
}).toThrow("[ERROR]");
});

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
test("로또 번호가 1부터 45 사이의 숫자가 아니면 예외가 발생한다.", () => {
expect(() => {
new Lotto([0, 2, 3, 4, 5, 6]);
}).toThrow("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");

expect(() => {
new Lotto([1, 2, 3, 4, 5, 46]);
}).toThrow("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
});

test("정상적인 로또 번호가 입력되면 예외가 발생하지 않는다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6]);
}).not.toThrow();
});

test("로또 번호가 오름차순으로 정렬된다.", () => {
const lotto = new Lotto([6, 5, 4, 3, 2, 1]);
expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5, 6]);
});
});
79 changes: 77 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
import { Console } from "@woowacourse/mission-utils";
import Lotto from "./Lotto.js";
import PrizeChecker from "./PrizeChecker.js";

class App {
async run() {}
async run() {
try {
const purchaseAmount = await this.getPurchaseAmount();
const lottoCount = purchaseAmount / 1000;
const lottos = this.generateLottos(lottoCount);

this.printLottoPurchaseDetails(lottoCount, lottos);

const winningNumbers = await this.getWinningNumbers();
const bonusNumber = await this.getBonusNumber();

this.printResultsAndRevenueRate(purchaseAmount, lottos, winningNumbers, bonusNumber);
} catch (error) {
Console.print(error.message);
}
}

async getUserInput(message) {
const input = await Console.readLineAsync(message);
return input.trim();
}

async getPurchaseAmount() {
const input = await this.getUserInput(`구입 금액을 입력해주세요.\n`);
const amount = parseInt(input);
this.validatePurchaseAmount(amount);
return amount;
}

validatePurchaseAmount(amount) {
if (isNaN(amount) || amount % 1000 !== 0) {
throw new Error("[ERROR] 천 원 단위로 입력해야 합니다.");
}
}

generateLottos(lottoCount) {
return Array.from({ length: lottoCount }, () => Lotto.generateRandomLotto());
}

async getWinningNumbers() {
const input = await this.getUserInput("당첨 번호를 입력해주세요. 번호는 쉼표(,)로 구분됩니다.\n");
const numbers = input.split(",").map(num => parseInt(num.trim()));
const winningLotto = Lotto.validateWinningNumbers(numbers);
return winningLotto.getNumbers();
}

async getBonusNumber() {
const input = await this.getUserInput("보너스 번호를 입력해주세요.\n");
const bonusNumber = parseInt(input);
Lotto.validateBonusNumber(bonusNumber);
return bonusNumber;
}

printLottoPurchaseDetails(lottoCount, lottos) {
Console.print(`${lottoCount}개를 구매했습니다.`);
lottos.forEach(lotto => Console.print(`[${lotto.getNumbers().join(", ")}]`));
}

printResultsAndRevenueRate(purchaseAmount, lottos, winningNumbers, bonusNumber) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 아래 부분부터는 PrizeChecker 클래스가 아니라 왜 App 클래스에 구현하신 건지 궁금합니다! 개인적으로 수익률 계산하는 곳에 묶어 놓았으면 더 보기 좋지 않을까 싶어서

const result = PrizeChecker.checkResults(lottos, winningNumbers, bonusNumber);
this.printResultDetails(result);
const revenueRate = PrizeChecker.calculateRevenueRate(purchaseAmount, result);
Console.print(`총 수익률은 ${revenueRate}%입니다.`);
}

printResultDetails(result) {
Console.print(`3개 일치 (5,000원) - ${result['3개 일치']}개`);
Console.print(`4개 일치 (50,000원) - ${result['4개 일치']}개`);
Console.print(`5개 일치 (1,500,000원) - ${result['5개 일치']}개`);
Console.print(`5개 일치, 보너스 볼 일치 (30,000,000원) - ${result['5개 일치 (보너스 볼 일치)']}개`);
Console.print(`6개 일치 (2,000,000,000원) - ${result['6개 일치']}개`);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

App.js 부분에서 사용자 UI 부분만 깔끔하게 정리해서 작성하면 좀 더 코드 분리가 잘 될 것 같습니다! 예외처리문 까지 App에 넣을 필요는 없을 것 같아요!


export default App;
export default App;
38 changes: 37 additions & 1 deletion src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Random } from "@woowacourse/mission-utils";

class Lotto {
#numbers;

Expand All @@ -10,9 +12,43 @@ class Lotto {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}
this.#checkNumberRange(numbers);
this.#checkDuplicates(numbers);
}

#checkNumberRange(numbers) {
if (!numbers.every(num => num >= 1 && num <= 45)) {
throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
}

#checkDuplicates(numbers) {
const uniqueNumbers = new Set(numbers);
if (uniqueNumbers.size !== 6) {
throw new Error("[ERROR] 로또 번호는 중복되지 않아야 합니다.");
}
}

static generateRandomLotto() {
return new Lotto(Random.pickUniqueNumbersInRange(1, 45, 6));
}

// TODO: 추가 기능 구현
getNumbers() {
return [...this.#numbers].sort((a, b) => a - b);
}

static validateWinningNumbers(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 당첨 번호는 6개여야 합니다.");
}
return new Lotto(numbers);
}

static validateBonusNumber(number) {
if (number < 1 || number > 45 || isNaN(number)) {
throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.");
}
}
}

export default Lotto;
52 changes: 52 additions & 0 deletions src/PrizeChecker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//수익률 계산
class PrizeChecker {
static checkResults(lottos, winningNumbers, bonusNumber) {
const result = {
'6개 일치': 0,
'5개 일치 (보너스 볼 일치)': 0,
'5개 일치': 0,
'4개 일치': 0,
'3개 일치': 0
};

lottos.forEach(lotto => {
const matchingNumbers = this.getMatchingCount(lotto.getNumbers(), winningNumbers);
this.updateResult(result, matchingNumbers, lotto.getNumbers(), bonusNumber);
});

return result;
}

static getMatchingCount(lottoNumbers, winningNumbers) {
return lottoNumbers.filter(number => winningNumbers.includes(number)).length;
}

static updateResult(result, matchingNumbers, lottoNumbers, bonusNumber) {
if (matchingNumbers === 6) result['6개 일치']++;
if (matchingNumbers === 5 && lottoNumbers.includes(bonusNumber)) {
result['5개 일치 (보너스 볼 일치)']++;
}
if (matchingNumbers === 5) result['5개 일치']++;
if (matchingNumbers === 4) result['4개 일치']++;
if (matchingNumbers === 3) result['3개 일치']++;
}

static calculateRevenueRate(purchaseAmount, result) {
const totalPrizeMoney = this.calculateTotalPrizeMoney(result);
return ((totalPrizeMoney / purchaseAmount) * 100).toFixed(1);
}

static calculateTotalPrizeMoney(result) {
const prizeMoney = {
'3개 일치': 5000,
'4개 일치': 50000,
'5개 일치': 1500000,
'5개 일치 (보너스 볼 일치)': 30000000,
'6개 일치': 2000000000
};

return Object.entries(result).reduce((total, [key, value]) => total + value * prizeMoney[key], 0);
}
}

export default PrizeChecker;
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import App from "./App.js";

const app = new App();
await app.run();
await app.run();