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
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,86 @@
# javascript-lotto-precourse
# 🎰 javascript-lotto-precourse

### 간단한 로또 발매기

---

## 📋 기능 요구 사항

1. **기본 기능**

- 로또 번호는 `1 ~ 45`까지의 숫자이다.
- 1개의 로또를 **발행**할 때 `중복되지 않는 6개의 숫자`를 뽑는다.
- **당첨 번호 추첨** 시 중복되지 않는 `6개 번호 + 보너스 번호 1개`를 입력받는다.
- 당첨은 `1 ~ 5등`까지 있으며, 당첨 기준과 상금은 아래와 같다.

| 등수 | 당첨기준 | 상금 |
| :--: | :-------------------------: | :-------------: |
| 1등 | 6개 번호 일치 | 2,000,000,000원 |
| 2등 | 5개 번호 + 보너스 번호 일치 | 30,000,000원 |
| 3등 | 5개 번호 일치 | 1,500,000원 |
| 4등 | 4개 번호 일치 | 50,000원 |
| 5등 | 3개 번호 일치 | 5,000원 |

- 사용자는 로또 구입 금액을 입력한다.
- 입력 받은 금액에 따라 로또를 발행한다. (1장 = 1,000원)
- 발행된 로또 번호는 **오름차순**으로 정렬되어 출력된다.
- **당첨 번호와 보너스 번호**를 입력받는다.
- 사용자가 구매한 로또 번호와 비교한 뒤 **당첨 내역 및 수익률을 출력**하고 프로그램을 종료한다.

2. **입력 예외 처리**

- 잘못된 입력이 들어온 경우 `[ERROR]`로 시작하는 메시지와 함께 `Error`를 발생시킨다.
- 프로그램 종료 시 `process.exit()`를 사용하지 않는다.
- 예외 발생 조건:

- 구입 금액 입력 예외

- 입력한 값이 숫자가 아니거나 1,000원으로 나누어 떨어지지 않는 경우
: `[ERROR] 구입 금액은 1,000원 단위의 숫자여야 합니다.` 출력 후 종료

- 당첨 번호 입력 예외

- 입력한 값의 범위가 1 ~ 45를 벗어나거나 숫자가 아닌 경우
: `[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.` 출력 후 종료
- 입력한 값 내에 중복된 번호가 있는 경우
: `[ERROR] 중복된 당첨 번호가 있습니다. : {중복 번호}` 출력 후 종료
- 당첨 번호의 개수가 6개가 아닌 경우
: `[ERROR] 로또 번호는 6개입니다.` 출력 후 종료

- 보너스 번호 입력 예외
- 1 ~ 45를 벗어나거나 숫자가 아닌 경우
: `[ERROR] 보너스 번호는 1 ~ 45 사이의 정수만 입력 가능합니다.` 출력 후 종료
- 보너스 번호가 당첨 번호와 중복된 경우
: `[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.` 출력 후 종료

3. **실행 결과 예시**

```bash
구입금액을 입력해 주세요.
8000

8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]

당첨 번호를 입력해 주세요.
1,2,3,4,5,6

보너스 번호를 입력해 주세요.
7

당첨 통계
---
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.
```
18 changes: 16 additions & 2 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,26 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});

// TODO: 테스트가 통과하도록 프로덕션 코드 구현
test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 5]);
}).toThrow("[ERROR]");
});

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
test("로또 번호가 6개 미만이면 예외가 발생한다.", () => {
expect(() => new Lotto([1, 2, 3, 4, 5])).toThrow("[ERROR]");
});

test("로또 번호가 숫자가 아닐 경우 예외가 발생한다.", () => {
expect(() => new Lotto(["a", 2, 3, 4, 5, 6])).toThrow("[ERROR]");
});

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("로또 번호가 6개이고 중복 없이 1~45 범위의 숫자면 정상 생성된다.", () => {
expect(() => new Lotto([1, 2, 3, 4, 5, 6])).not.toThrow();
});
});
42 changes: 41 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
import { Console } from "@woowacourse/mission-utils";

import {
getPurchasePrice,
getLottoNumbers,
getBonusNumber,
} from "./utils/input.js";
import { generateLotto, printLottos } from "./utils/generateLotto.js";
import {
validateBonusNumber,
validateLottoNumber,
validatePrice,
} from "./utils/validation.js";
import { calculateResult } from "./utils/calculateResult.js";

import { ERROR_MESSAGES } from "./utils/constants.js";

class App {
async run() {}
async run() {
try {
const inputPrice = await getPurchasePrice();
const totalPrice = validatePrice(inputPrice);

const lottos = generateLotto(totalPrice);
printLottos(lottos);

const inputLottoNumbers = await getLottoNumbers();
const winnerLotto = validateLottoNumber(inputLottoNumbers);

const inputBonusNumber = await getBonusNumber();
const bonusNumber = validateBonusNumber(winnerLotto, inputBonusNumber);

calculateResult(lottos, winnerLotto, bonusNumber);
} catch (error) {
if (!error.message.startsWith("[ERROR]")) {
Console.print(`[ERROR] ${error.message}`);
} else {
Console.print(error.message);
}
return;
}
}
}

export default App;
34 changes: 31 additions & 3 deletions src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,46 @@
import { ERROR_MESSAGES } from "./utils/constants.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개여야 합니다.");
throw new Error(ERROR_MESSAGES.LOTTO_LENGTH);
}

const duplicateNumber = numbers.filter(
(number, index) => numbers.indexOf(number) !== index
);

if (duplicateNumber.length > 0) {
throw new Error(
`${ERROR_MESSAGES.LOTTO_DUPLICATE} : ${duplicateNumber.join(", ")}`
);
}

if (
numbers.some(
(number) =>
isNaN(number) ||
number < 1 ||
number > 45 ||
!Number.isInteger(number) ||
number === ""
)
) {
throw new Error(ERROR_MESSAGES.LOTTO_RANGE);
}
}

// TODO: 추가 기능 구현
// 로또 번호 반환
getNumbers() {
return this.#numbers;
}
}

export default Lotto;
43 changes: 43 additions & 0 deletions src/utils/calculateResult.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Console } from "@woowacourse/mission-utils";

const PRIZE_INFO = {
3: { label: "3개 일치 (5,000원)", prize: 5000 },
4: { label: "4개 일치 (50,000원)", prize: 50000 },
5: { label: "5개 일치 (1,500,000원)", prize: 1500000 },
bonus: { label: "5개 일치, 보너스 볼 일치 (30,000,000원)", prize: 30000000 },
6: { label: "6개 일치 (2,000,000,000원)", prize: 2000000000 },
};

export function calculateResult(lottos, winnerLotto, bonusNumber) {
const winningNumbers = winnerLotto.getNumbers();
const result = { 3: 0, 4: 0, 5: 0, 6: 0, bonus: 0 };

lottos.forEach((lotto) => {
const matched = lotto
.getNumbers()
.filter((n) => winningNumbers.includes(n)).length;

if (matched === 5 && lotto.getNumbers().includes(bonusNumber)) {
result.bonus += 1;
} else if (matched >= 3) {
result[matched] += 1;
}
});

printResult(result, lottos.length);
}

function printResult(result, totalCount) {
Console.print("\n당첨 통계");
Console.print("---");

let totalPrize = 0;
for (const [key, { label, prize }] of Object.entries(PRIZE_INFO)) {
const count = result[key] || 0;
Console.print(`${label} - ${count}개`);
totalPrize += count * prize;
}

const rate = ((totalPrize / (totalCount * 1000)) * 100).toFixed(1);
Console.print(`총 수익률은 ${rate}%입니다.`);
}
8 changes: 8 additions & 0 deletions src/utils/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const ERROR_MESSAGES = {
PRICE: "[ERROR] 구입 금액은 1000원 단위의 숫자여야 합니다.",
LOTTO_RANGE: "[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.",
LOTTO_DUPLICATE: "[ERROR] 중복된 당첨 번호가 있습니다.",
LOTTO_LENGTH: "[ERROR] 로또 번호는 6개 입니다.",
BONUS_RANGE: "[ERROR] 보너스 번호눈 1 ~ 45 사이의 정수만 입력 가능합니다.",
BONUS_DUPLICATE: "[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.",
};
21 changes: 21 additions & 0 deletions src/utils/generateLotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Console, Random } from "@woowacourse/mission-utils";
import Lotto from "../Lotto.js";

export function generateLotto(price) {
const count = price / 1000;
const lottoNumbers = [];

for (let i = 0; i < count; i++) {
const numbers = Random.pickUniqueNumbersInRange(1, 45, 6);
lottoNumbers.push(new Lotto(numbers));
}

return lottoNumbers;
}

export function printLottos(lottos) {
Console.print(`\n${lottos.length}개를 구매했습니다.`);
lottos.forEach((lotto) => {
Console.print(`[${lotto.getNumbers().join(", ")}]`);
});
}
13 changes: 13 additions & 0 deletions src/utils/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Console } from "@woowacourse/mission-utils";

export async function getPurchasePrice() {
return Console.readLineAsync("구입금액을 입력해 주세요.\n");
}

export async function getLottoNumbers() {
return Console.readLineAsync("\n당첨 번호를 입력해 주세요.\n");
}

export async function getBonusNumber() {
return Console.readLineAsync("\n보너스 번호를 입력해 주세요.\n");
}
30 changes: 30 additions & 0 deletions src/utils/validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ERROR_MESSAGES } from "./constants.js";
import Lotto from "../Lotto.js";

export function validatePrice(price) {
if (isNaN(price) || price <= 0 || price % 1000 !== 0) {
throw new Error(ERROR_MESSAGES.PRICE);
}

return price;
}

export function validateLottoNumber(inputLotto) {
const lottoNumbers = inputLotto.split(",").map((num) => Number(num.trim()));

const winnerLotto = new Lotto(lottoNumbers);
return winnerLotto;
}

export function validateBonusNumber(winnerLotto, inputBonus) {
const bonusNumber = Number(inputBonus.trim());

if (isNaN(bonusNumber) || bonusNumber < 1 || bonusNumber > 45) {
throw new Error(ERROR_MESSAGES.BONUS_RANGE);
}

if (winnerLotto.getNumbers().includes(bonusNumber)) {
throw new Error(ERROR_MESSAGES.BONUS_DUPLICATE);
}
return bonusNumber;
}