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

콘솔 기반 로또 발매 및 당첨 결과 확인 프로그램

사용자가 구입 금액을 입력하면, 자동으로 로또를 발행하고

당첨 번호를 입력받아 결과 및 수익률을 출력합니다.


🚀 프로젝트 개요

이 프로그램은 사용자가 입력한 금액에 맞게 로또를 발행하고,

당첨 번호와 비교하여 각 등수별 당첨 내역과 총 수익률을 계산합니다.

입력 검증, 예외 처리, 난수 생성 등 기본적인 로직 설계 능력을 검증하기 위한 콘솔 미션입니다.


📝 기능 목록

[V] 로또 구입 금액 입력

[V] 로또 발행

[V] Lotto 클래스

[V] 로또 출력

[V] 당첨 번호 입력

[V] 보너스 번호 입력

[V] 당첨 내역 계산

[V] 수익률 계산 및 출력
81 changes: 79 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,82 @@
import InputView from './InputView.js';
import OutputView from './OutputView.js';
import Lotto from './Lotto.js';
import Validator from './Validator.js';
import LottoIssuer from './LottoIssuer.js';
import StatisticsCalculator from './StatisticsCalculator.js';

class App {
async run() {}
#lottos;
#purchaseAmount;

async run() {
await this.#getPurchaseAmount();
this.#issueLottos();

const winningLotto = await this.#getWinningLotto();
const bonusNumber = await this.#getBonusNumber(winningLotto);

this.#showResults(winningLotto, bonusNumber);
}

async #getPurchaseAmount() {
while (true) {
try {
const amountStr = await InputView.readPurchaseAmount();
const amount = Number(amountStr);
Validator.validatePurchaseAmount(amount);
this.#purchaseAmount = amount;
return;
} catch (error) {
OutputView.printError(error.message);
}
}
}

#issueLottos() {
this.#lottos = LottoIssuer.issueLottos(this.#purchaseAmount);
OutputView.printPurchaseCount(this.#lottos.length);
OutputView.printLottos(this.#lottos);
}

async #getWinningLotto() {
while (true) {
try {
const numbersStr = await InputView.readWinningNumbers();
const numbers = numbersStr.split(',').map(Number);
Validator.validateWinningNumbers(numbers);
return new Lotto(numbers);
} catch (error) {
OutputView.printError(error.message);
}
}
}

async #getBonusNumber(winningLotto) {
while (true) {
try {
const bonusStr = await InputView.readBonusNumber();
const bonusNumber = Number(bonusStr);
Validator.validateBonusNumber(bonusNumber, winningLotto.getNumbers());
return bonusNumber;
} catch (error) {
OutputView.printError(error.message);
}
}
}

#showResults(winningLotto, bonusNumber) {
const results = StatisticsCalculator.calculateResults(
this.#lottos, winningLotto, bonusNumber
);

const profitRate = StatisticsCalculator.calculateProfitRate(
results, this.#purchaseAmount
);

OutputView.printStatistics(results);
OutputView.printProfitRate(profitRate);
}
}

export default App;
export default App;
26 changes: 26 additions & 0 deletions src/Constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const LOTTO_RULES = Object.freeze({
NUMBER_COUNT: 6,
MIN_NUMBER: 1,
MAX_NUMBER: 45,
PRICE: 1000,
});

export const ERROR_MESSAGES = Object.freeze({
PREFIX: '[ERROR]',
NOT_A_NUMBER: '숫자를 입력해야 합니다.',
INVALID_AMOUNT_UNIT: `구입 금액은 ${LOTTO_RULES.PRICE}원 단위여야 합니다.`,
AMOUNT_LESS_THAN_PRICE: `구입 금액은 ${LOTTO_RULES.PRICE}원 이상이어야 합니다.`,
INVALID_NUMBER_COUNT: `로또 번호는 ${LOTTO_RULES.NUMBER_COUNT}개여야 합니다.`,
DUPLICATE_NUMBERS: '로또 번호에 중복된 숫자가 있습니다.',
OUT_OF_RANGE: `로또 번호는 ${LOTTO_RULES.MIN_NUMBER}부터 ${LOTTO_RULES.MAX_NUMBER} 사이의 숫자여야 합니다.`,
DUPLICATE_WITH_WINNING_NUMBERS: '보너스 번호가 당첨 번호와 중복됩니다.',
});

export const WINNING_RANKS = Object.freeze({
FIRST: { match: 6, prize: 2_000_000_000, name: 'FIRST' },
SECOND: { match: 5, prize: 30_000_000, name: 'SECOND' },
THIRD: { match: 5, prize: 1_500_000, name: 'THIRD' },
FOURTH: { match: 4, prize: 50_000, name: 'FOURTH' },
FIFTH: { match: 3, prize: 5_000, name: 'FIFTH' },
NONE: { match: 0, prize: 0, name: 'NONE' },
});
18 changes: 18 additions & 0 deletions src/InputView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Console } from '@woowacourse/mission-utils';
import { PROMPT_MESSAGES } from './Constants.js';

const InputView = {
async readPurchaseAmount() {
return Console.readLineAsync(PROMPT_MESSAGES.GET_PURCHASE_AMOUNT);
},

async readWinningNumbers() {
return Console.readLineAsync(PROMPT_MESSAGES.GET_WINNING_NUMBERS);
},

async readBonusNumber() {
return Console.readLineAsync(PROMPT_MESSAGES.GET_BONUS_NUMBER);
},
};

export default InputView;
48 changes: 44 additions & 4 deletions src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,58 @@
import { LOTTO_RULES, ERROR_MESSAGES, WINNING_RANKS } from "./Constants";

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개여야 합니다.");
if (numbers.length !== LOTTO_RULES.NUMBER_COUNT) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.INVALID_NUMBER_COUNT}`);
}

if (new Set(numbers).size !== LOTTO_RULES.NUMBER_COUNT) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.DUPLICATE_NUMBERS}`);
}

if (numbers.some(n => n < LOTTO_RULES.MIN_NUMBER || n > LOTTO_RULES.MAX_NUMBER)) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.OUT_OF_RANGE}`);
}
}

getNumbers() {
return this.#numbers.#numbers;
}

calculateRank(winningLotto, bonusNumber) {
const winningNumbers = winningLotto.getNumbers();

const matchCount = this.#numbers.filter(number =>
winningNumbers.includes(number)
).length;

const hasBonus = this.#numbers.includes(bonusNumber);

if (matchCount === WINNING_RANKS.FIRST.match) {
return WINNING_RANKS.FIRST.name;
}
if (matchCount === WINNING_RANKS.SECOND.match && hasBonus) {
return WINNING_RANKS.SECOND.name;
}
if (matchCount === WINNING_RANKS.THIRD.match) {
return WINNING_RANKS.THIRD.name;
}
if (matchCount === WINNING_RANKS.FOURTH.match) {
return WINNING_RANKS.FOURTH.name;
}
if (matchCount === WINNING_RANKS.FIFTH.match) {
return WINNING_RANKS.FIFTH.name;
}
return WINNING_RANKS.NONE.name;
}

// TODO: 추가 기능 구현
}

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

const LottoIssuer = {
issueLottos(purchaseAmount) {
const count = purchaseAmount / LOTTO_RULES.PRICE;
const lottos = [];

for (let i = 0; i < count; i += 1) {
const numbers = Random.pickUniqueNumbersInRange(
LOTTO_RULES.MIN_NUMBER,
LOTTO_RULES.MAX_NUMBER,
LOTTO_RULES.NUMBER_COUNT
);
lottos.push(new Lotto(numbers));
}
return lottos;
},
};

export default LottoIssuer;
40 changes: 40 additions & 0 deletions src/OutputView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Console } from '@woowacourse/mission-utils';
import { WINNING_RANKS } from './Constants.js';

function formatPrize(prize) {
return prize.toLocaleString('ko-KR');
}

const OutputView = {
printPurchaseCount(count) {
Console.print(`\n${count}개를 구매했습니다.`);
},

printLottos(lottos) {
lottos.forEach(lotto => {
Console.print(`[${lotto.getNumbers().join(', ')}]`);
});
},

printStatistics(results) {
Console.print('\n당첨 통계\n---');

const R = WINNING_RANKS;

Console.print(`3개 일치 (${formatPrize(R.FIFTH.prize)}원) - ${results[R.FIFTH.name]}개`);
Console.print(`4개 일치 (${formatPrize(R.FOURTH.prize)}원) - ${results[R.FOURTH.name]}개`);
Console.print(`5개 일치 (${formatPrize(R.THIRD.prize)}원) - ${results[R.THIRD.name]}개`);
Console.print(`5개 일치, 보너스 볼 일치 (${formatPrize(R.SECOND.prize)}원) - ${results[R.SECOND.name]}개`);
Console.print(`6개 일치 (${formatPrize(R.FIRST.prize)}원) - ${results[R.FIRST.name]}개`);
},

printProfitRate(profitRate) {
Console.print(`총 수익률은 ${profitRate}%입니다.`);
},

printError(message) {
Console.print(message);
},
};

export default OutputView;
38 changes: 38 additions & 0 deletions src/StatisticsCalculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { WINNING_RANKS } from './Constants.js';

const StatisticsCalculator = {
calculateResults(lottos, winningLotto, bonusNumber) {
const results = {
[WINNING_RANKS.FIRST.name]: 0,
[WINNING_RANKS.SECOND.name]: 0,
[WINNING_RANKS.THIRD.name]: 0,
[WINNING_RANKS.FOURTH.name]: 0,
[WINNING_RANKS.FIFTH.name]: 0,
};

lottos.forEach(lotto => {
const rank = lotto.calculateRank(winningLotto, bonusNumber);
if (rank !== WINNING_RANKS.NONE.name) {
results[rank] += 1;
}
});

return results;
},

calculateProfitRate(results, purchaseAmount) {
let totalPrize = 0;

Object.values(WINNING_RANKS).forEach(rankInfo => {
if (rankInfo.name !== WINNING_RANKS.NONE.name) {
totalPrize += results[rankInfo.name] * rankInfo.prize;
}
});

const profitRate = (totalPrize / purchaseAmount) * 100;

return Math.round(profitRate * 10) / 10;
},
};

export default StatisticsCalculator;
38 changes: 38 additions & 0 deletions src/Validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { LOTTO_RULES, ERROR_MESSAGES } from "./Constants";
import Lotto from "./Lotto";

const Validator = {
validatePurchaseAmount(amount) {
if (isNaN(amount)) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.NOT_A_NUMBER}`);
}
if (amount < LOTTO_RULES.PRICE) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.AMOUNT_LESS_THAN_PRICE}`);
}
if (amount % LOTTO_RULES.PRICE !== 0) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.INVALID_AMOUNT_UNIT}`);
}
},

validateWinningNumbers(numbers) {
try {
new Lotto(numbers);
} catch (error) {
throw error;
}
},

validateBonusNumber(bonusNumber, winningNumbers) {
if (isNaN(bonusNumber)) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.NOT_A_NUMBER}`);
}
if (bonusNumber < LOTTO_RULES.MIN_NUMBER || bonusNumber > LOTTO_RULES.MAX_NUMBER) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.OUT_OF_RANGE}`);
}
if (winningNumbers.includes(bonusNumber)) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.DUPLICATE_WITH_WINNING_NUMBERS}`);
}
},
};

export default Validator;
Loading