Skip to content
115 changes: 114 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,114 @@
# javascript-lotto-precourse
# 로또 (javascript-lotto-precourse)

## 🍥 기능 요구 사항
초간단 자동차 경주 게임을 구현한다.

- [ ] 로또 번호의 숫자 범위는 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원이다.

- [ ] 당첨 번호와 보너스 번호를 입력받는다.

- [ ] 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.

- 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 해당 지점부터 다시 입력을 받는다.
- [ ] 숫자 범위가 1~45를 넘어가게 입력한 경우
- [ ] 숫자가 아닌 문자를 입력한 경우
- [ ] 구입 금액이 1,000원으로 나누어 떨어지지 않는 경우
- [ ] 중복된 숫자가 있는 경우
- [ ] 로또 번호를 6개 이상 입력한 경우


## 🍥 입출력 요구 사항
- 입력
- 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.
```jsx
14000
```

- 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.
```jsx
1,2,3,4,5,6
```

- 보너스 번호를 입력 받는다.
```jsx
7
```

- 출력
- 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다.
```jsx
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]
```

- 당첨 내역을 출력한다.
```jsx
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
```

- 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
```jsx
총 수익률은 62.5%입니다.
```

- 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
```jsx
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
```

- 실행 결과 예시
```jsx
구입금액을 입력해 주세요.
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%입니다.
```
14 changes: 14 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,18 @@ describe("로또 클래스 테스트", () => {
});

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
test("로또 번호에 숫자가 아닌 값이 포함되면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, "a"]);
}).toThrow("[ERROR]");
expect(() => {
new Lotto([1, 2, 3, 4, 5, null]);
}).toThrow("[ERROR]");
});

test("로또 번호가 6개의 고유한 숫자로 이루어져 있을 경우 예외가 발생하지 않는다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6]);
}).not.toThrow();
});
});
75 changes: 74 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,78 @@
import ConsoleUtil from "./utils/ConsoleUtil.js";
import LottoManager from "./LottoManager.js";
import errorMessages from "./errors/errorMessages.js";
import ResultPrinter from "./ResultPrinter.js";

class App {
async run() {}
constructor() {
this.lottoManager = new LottoManager();
}

async run() {
try {
const amount = await ConsoleUtil.readLine("구입금액을 입력해 주세요.\n");
this.#validateAmount(amount);
const lottoCount = Number(amount) / 1000;
const lottos = this.lottoManager.generateLottos(lottoCount);

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

await this.#getWinningNumbers(lottoCount);
} catch (error) {
ConsoleUtil.print(error.message);
await this.run();
}
}

#validateAmount(amount) {
const num = Number(amount);
if (isNaN(num) || num % 1000 !== 0) {
throw new Error(errorMessages.INVALID_MONEY_ERROR);
}
return num;
}

async #getWinningNumbers(lottoCount) {
try {
const winningNumbersInput = await ConsoleUtil.readLine("당첨 번호를 입력해 주세요.\n");
const winningNumbers = this.#parseNumbers(winningNumbersInput);

const bonusNumberInput = await ConsoleUtil.readLine("보너스 번호를 입력해 주세요.\n");
const bonusNumber = this.#parseBonusNumber(bonusNumberInput, winningNumbers);

const results = this.lottoManager.calculateResults(winningNumbers, bonusNumber);
ResultPrinter.printResults(results, lottoCount);
} catch (error) {
ConsoleUtil.print(error.message);
await this.#getWinningNumbers(lottoCount);
}
}

#parseNumbers(input) {
const numbers = input.split(",").map(Number);
if (numbers.length !== 6) {
throw new Error(errorMessages.AMOUNT_OVER_ERROR);
}
if (new Set(numbers).size !== 6) {
throw new Error(errorMessages.SAME_NUMBER_ERROR);
}
if (numbers.some((num) => isNaN(num))) {
throw new Error(errorMessages.NOT_NUMBER_ERROR);
}
if (numbers.some((num) => num < 1 || num > 45)) {
throw new Error(errorMessages.RANGE_OVER_ERROR);
}
return numbers;
}

#parseBonusNumber(input, winningNumbers) {
const bonusNumber = Number(input);
if (isNaN(bonusNumber) || bonusNumber < 1 || bonusNumber > 45 || winningNumbers.includes(bonusNumber)) {
throw new Error(errorMessages.RANGE_OVER_ERROR);
}
return bonusNumber;
}
}

export default App;
17 changes: 15 additions & 2 deletions src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import errorMessages from "./errors/errorMessages.js";

class Lotto {
#numbers;

Expand All @@ -8,11 +10,22 @@ class Lotto {

#validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
throw new Error(errorMessages.AMOUNT_OVER_ERROR);
}
if (new Set(numbers).size !== 6) {
throw new Error(errorMessages.SAME_NUMBER_ERROR);
}
if (numbers.some((num) => isNaN(num))) {
throw new Error(errorMessages.NOT_NUMBER_ERROR);
}
if (numbers.some((num) => num < 1 || num > 45)) {
throw new Error(errorMessages.RANGE_OVER_ERROR);
}
}

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

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

class LottoManager {
constructor() {
this.lottos = [];
}

generateLottos(count) {
this.lottos = Array.from({ length: count }, () => {
const numbers = Random.pickUniqueNumbersInRange(1, 45, 6);
return new Lotto(numbers);
});
return this.lottos;
}

calculateResults(winningNumbers, bonusNumber) {
const results = { 3: 0, 4: 0, 5: 0, "5_bonus": 0, 6: 0 };
this.lottos.forEach((lotto) => {
const matchCount = lotto.getNumbers().filter((num) => winningNumbers.includes(num)).length;
const isBonusMatch = lotto.getNumbers().includes(bonusNumber);

if (matchCount === 6) {
results[6]++;
} else if (matchCount === 5 && isBonusMatch) {
results["5_bonus"]++;
} else if (matchCount === 5) {
results[5]++;
} else if (matchCount === 4) {
results[4]++;
} else if (matchCount === 3) {
results[3]++;
}
});
return results;
}
}

export default LottoManager;
27 changes: 27 additions & 0 deletions src/ResultPrinter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ConsoleUtil from "./utils/ConsoleUtil.js";

export default class ResultPrinter {
static printResults(results, lottoCount) {
const prizeMap = {
3: { amount: 5000, label: "3개 일치 (5,000원)" },
4: { amount: 50000, label: "4개 일치 (50,000원)" },
5: { amount: 1500000, label: "5개 일치 (1,500,000원)" },
"5_bonus": { amount: 30000000, label: "5개 일치, 보너스 볼 일치 (30,000,000원)" },
6: { amount: 2000000000, label: "6개 일치 (2,000,000,000원)" },
};

ConsoleUtil.print("당첨 통계\n---");
let totalEarnings = 0;

Object.keys(prizeMap).forEach((key) => {
const count = results[key] || 0;
const prize = prizeMap[key];
totalEarnings += count * prize.amount;
ConsoleUtil.print(`${prize.label} - ${count}개`);
});

const investment = lottoCount * 1000;
const profitRate = ((totalEarnings / investment) * 100).toFixed(1);
ConsoleUtil.print(`총 수익률은 ${profitRate}%입니다.`);
}
}
9 changes: 9 additions & 0 deletions src/errors/errorMessages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const errorMessages = {
RANGE_OVER_ERROR: "[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.",
NOT_NUMBER_ERROR: "[ERROR] 로또 번호는 숫자여야 합니다.",
INVALID_MONEY_ERROR: "[ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다.",
SAME_NUMBER_ERROR: "[ERROR] 로또 번호는 중복되지 않는 6개의 숫자여야 합니다.",
AMOUNT_OVER_ERROR: "[ERROR] 로또 번호는 6개여야 합니다.",
};

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

const ConsoleUtil = {
print(message) {
Console.print(message);
},
async readLine(prompt) {
return await Console.readLineAsync(prompt);
},
};

export default ConsoleUtil;