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

- 간단한 로또 발매기를 구현한 프로젝트이다.

## 주요 기능

### 1. 사용자 입력 처리

- [x] 구입 금액 입력 받기
- [x] 당첨 번호 입력 받기
- [x] 보너스 번호 입력 받기

### 2. 발행된 로또 출력 기능

- [x] 입력 받은 금액 / 1000을 통해 출력할 로또의 개수 출력
- [x] 로또 당 중복 없는 6개의 번호 랜덤 출력

### 3. 로또 당첨 결과 출력 기능

- [x] 발행된 로또와 입력 받은 번호들의 중복 확인
- [x] 일치하는 개수에 따라 당첨 통계 출력
- [x] 당첨금 / 투자금 \* 100의 식으로 수익률 출력
- [x] 소수점 둘째 자리에서 반올림해야 한다

### 4. 예외처리

- 구입 금액 예외
- [x] 구입 금액은 1000으로 나누어 떨어져야 한다
- 당첨 번호 예외
- [x] 각 번호가 1~45 사이의 중복되지 않은 6개의 숫자여야 한다
- [x] ,(쉼표)를 기준으로 입력되어야 한다
- 보너스 번호 예외
- [x] 당첨 번호와 다른 1~45 사이의 숫자여야 한다
23 changes: 20 additions & 3 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Lotto from "../src/Lotto";
import Lotto from "../src/Lotto.js";

describe("로또 클래스 테스트", () => {
test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
Expand All @@ -7,12 +7,29 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});

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

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

test('당첨 번호와 일치하는 번호 개수를 올바르게 계산한다.', () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const winningNumbers = [1, 2, 3, 7, 8, 9];
const { matchCount } = lotto.match(winningNumbers, 10);
expect(matchCount).toBe(3);
});

test('보너스 번호 일치 여부를 올바르게 판단한다.', () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const winningNumbers = [1, 2, 3, 4, 5, 7];
const { hasBonus } = lotto.match(winningNumbers, 6);
expect(hasBonus).toBe(true);
});
});
138 changes: 137 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,141 @@
import { LOTTO, PRIZE, MESSAGE, ERROR_MESSAGE } from "./utils/constants.js";
import {
validatePurchaseAmount,
validateWinningNumbers,
validateBonusNumber,
} from "./utils/validation.js";
import {
generateLottoNumbers,
readLineAsync,
print,
} from "./utils/missionUtil.js";
import Lotto from "./Lotto.js";

class App {
async run() {}
#lottos = [];
#winningNumbers = [];
#bonusNumber = 0;

async run() {
await this.purchaseLottos();
await this.inputWinningNumbers();
await this.inputBonusNumber();
this.showResult();
}

async purchaseLottos() {
const amount = validatePurchaseAmount(
await readLineAsync(MESSAGE.PURCHASE_MESSAGE)
);

const count = amount / LOTTO.PRICE;
this.generateLottos(count);

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

generateLottos(count) {
this.#lottos = Array.from(
{ length: count },
() => new Lotto(generateLottoNumbers())
);
}

async inputWinningNumbers() {
this.#winningNumbers = validateWinningNumbers(
await readLineAsync(MESSAGE.WINNING_NUMBERS_MESSAGE)
);
}

async inputBonusNumber() {
this.#bonusNumber = validateBonusNumber(
await readLineAsync(MESSAGE.BONUS_NUMBER_MESSAGE),
this.#winningNumbers
);
}

showResult() {
const results = this.calculateResults();
const profit = this.calculateProfit(results);

print("\n당첨 통계\n---");
this.printPrizeResults(results);
this.printProfitRate(profit);
}

calculateResults() {
const results = new Map([
[PRIZE.FIFTH.MATCH, 0],
[PRIZE.FOURTH.MATCH, 0],
[PRIZE.THIRD.MATCH, 0],
[PRIZE.SECOND.MATCH, 0], // 5 matches + bonus
[PRIZE.FIRST.MATCH, 0],
]);

this.#lottos.forEach((lotto) => {
const { matchCount, hasBonus } = lotto.match(
this.#winningNumbers,
this.#bonusNumber
);

if (matchCount === PRIZE.SECOND.MATCH && hasBonus) {
results.set(PRIZE.SECOND.MATCH, results.get(PRIZE.SECOND.MATCH) + 1);
} else {
results.set(matchCount, results.get(matchCount) || 0 + 1);
}
});

return results;
}

calculateProfit(results) {
const investment = this.#lottos.length * LOTTO.PRICE;
let prize = 0;

prize += results.get(PRIZE.FIFTH.MATCH) * PRIZE.FIFTH.AMOUNT;
prize += results.get(PRIZE.FOURTH.MATCH) * PRIZE.FOURTH.AMOUNT;
prize += results.get(PRIZE.THIRD.MATCH) * PRIZE.THIRD.AMOUNT;
prize += results.get(PRIZE.SECOND.MATCH) * PRIZE.SECOND.AMOUNT;
prize += results.get(PRIZE.FIRST.MATCH) * PRIZE.FIRST.AMOUNT;

return (prize / investment) * 100;
}

printPrizeResults(results) {
print(
`3개 일치 (${PRIZE.FIFTH.AMOUNT.toLocaleString()}원) - ${results.get(
PRIZE.FIFTH.MATCH
)}개`
);
print(
`4개 일치 (${PRIZE.FOURTH.AMOUNT.toLocaleString()}원) - ${results.get(
PRIZE.FOURTH.MATCH
)}개`
);
print(
`5개 일치 (${PRIZE.THIRD.AMOUNT.toLocaleString()}원) - ${results.get(
PRIZE.THIRD.MATCH
)}개`
);
print(
`5개 일치, 보너스 볼 일치 (${PRIZE.SECOND.AMOUNT.toLocaleString()}원) - ${results.get(
PRIZE.SECOND.MATCH
)}개`
);
print(
`6개 일치 (${PRIZE.FIRST.AMOUNT.toLocaleString()}원) - ${results.get(
PRIZE.FIRST.MATCH
)}개`
);
}

printProfitRate(profit) {
print(`총 수익률은 ${profit.toFixed(1)}%입니다.`);
}
}

export default App;
24 changes: 17 additions & 7 deletions src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { validateNumbers } from "./utils/validation.js";
import { PRIZE } from "./utils/constants.js";

class Lotto {
#numbers;

constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
validateNumbers(numbers);
this.#numbers = numbers.sort((a, b) => a - b);
}

#validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}
match(winningNumbers, bonusNumber) {
const matchCount = this.#numbers.filter((number) =>
winningNumbers.includes(number)
).length;

const hasBonus =
matchCount === PRIZE.SECOND.MATCH && this.#numbers.includes(bonusNumber);

return { matchCount, hasBonus };
}

// TODO: 추가 기능 구현
getNumbers() {
return [...this.#numbers];
}
}

export default Lotto;
31 changes: 31 additions & 0 deletions src/utils/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const LOTTO = {
PRICE: 1000,
LENGTH: 6,
MIN_NUMBER: 1,
MAX_NUMBER: 45,
};

export const PRIZE = {
FIRST: { MATCH: 6, AMOUNT: 2000000000 },
SECOND: { MATCH: 5, BONUS: true, AMOUNT: 30000000 },
THIRD: { MATCH: 5, AMOUNT: 1500000 },
FOURTH: { MATCH: 4, AMOUNT: 50000 },
FIFTH: { MATCH: 3, AMOUNT: 5000 },
};

export const MESSAGE = {
PURCHASE_MESSAGE: "구입금액을 입력해 주세요.\n",
WINNING_NUMBERS_MESSAGE: "당첨 번호를 입력해 주세요.\n",
BONUS_NUMBER_MESSAGE: "\n보너스 번호를 입력해 주세요.\n",
};

export const ERROR_MESSAGE = {
INVALID_PURCHASE_AMOUNT: "[ERROR] 구입 금액은 1000원 단위여야 합니다.",
INVALID_NUMBERS_LENGTH: "[ERROR] 로또 번호는 6개여야 합니다.",
INVALID_WINNING_NUMBERS:
"[ERROR] 당첨 번호는 쉼표로 구분된 6개의 숫자여야 합니다.",
INVALID_NUMBER_RANGE: "[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.",
INVALID_BONUS_NUMBER:
"[ERROR] 보너스 번호는 당첨 번호와 중복되지 않는 1부터 45 사이의 숫자여야 합니다.",
DUPLICATE_NUMBERS: "[ERROR] 중복된 숫자는 사용할 수 없습니다.",
};
18 changes: 18 additions & 0 deletions src/utils/missionUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Console, Random } from "@woowacourse/mission-utils";
import { LOTTO } from "./constants.js";

export const readLineAsync = async (message) => {
return Console.readLineAsync(message);
};

export const print = (message) => {
return Console.print(message);
};

export const generateLottoNumbers = () => {
return Random.pickUniqueNumbersInRange(
LOTTO.MIN_NUMBER,
LOTTO.MAX_NUMBER,
LOTTO.LENGTH
).sort((a, b) => a - b);
};
39 changes: 39 additions & 0 deletions src/utils/validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { LOTTO, ERROR_MESSAGE } from "./constants.js";

export const validatePurchaseAmount = (amount) => {
const number = Number(amount);
if (isNaN(number) || number % LOTTO.PRICE !== 0 || number < LOTTO.PRICE) {
throw new Error(ERROR_MESSAGE.INVALID_PURCHASE_AMOUNT);
}
return number;
};

export const validateWinningNumbers = (input) => {
const numbers = input.split(",").map((num) => Number(num.trim()));
validateNumbers(numbers);
return numbers;
};

export const validateBonusNumber = (number, winningNumbers) => {
const bonusNumber = Number(number);
if (
isNaN(bonusNumber) ||
bonusNumber < LOTTO.MIN_NUMBER ||
bonusNumber > LOTTO.MAX_NUMBER ||
winningNumbers.includes(bonusNumber)
) {
throw new Error(ERROR_MESSAGE.INVALID_BONUS_NUMBER);
}
return bonusNumber;
};

export const validateNumbers = (numbers) => {
const uniqueNumbers = new Set(numbers);
if (
numbers.length !== LOTTO.LENGTH ||
uniqueNumbers.size !== LOTTO.LENGTH ||
!numbers.every((num) => num >= LOTTO.MIN_NUMBER && num <= LOTTO.MAX_NUMBER)
) {
throw new Error(ERROR_MESSAGE.INVALID_NUMBER_RANGE);
}
};