지난 번 경험을 교훈 삼아, 이번에는 앱을 작은 단위로 쪼개서 하나씩 구현하고, 직접 테스트해보며 진행하기로 했다.
async #retryOnFailure(func) {
try {
return await func();
} catch (error) {
this.#outputView.print(error.message);
return this.#retryOnFailure(func);
}
}
async readMoneyAmount() {
const moneyAmountInput = await this.#retryOnFailure(() =>
this.#inputView.readInteger(MESSAGE.read.moneyAmount),
);
return Number(moneyAmountInput);
}
async start() {
const moneyAmount = await this.#view.readMoneyAmount();
Console.print(moneyAmount);
const winningNumbers = await this.#view.readWinningNumbers();
Console.print(winningNumbers);
const bonusNumber = await this.#view.readBonusNumber();
Console.print(bonusNumber);
}
class LottoGameController {
#view = new View();
#lottoPublisher = new LottoPublisher();
async start() {
await this.setMoneyAmountFromInput();
const winningNumbers = await this.#view.readWinningNumbers();
Console.print(winningNumbers);
const bonusNumber = await this.#view.readBonusNumber();
Console.print(bonusNumber);
}
async #retryOnFailure(func) {
try {
return await func();
} catch (error) {
this.#view.printError(error);
return this.#retryOnFailure(func);
}
}
async setMoneyAmountFromInput() {
await this.#retryOnFailure(async () => {
const moneyAmount = await this.#view.readMoneyAmount();
this.#lottoPublisher.setMoneyAmount(moneyAmount);
Console.print('잘 됐음');
});
}
}
//LottoPublisher.js
class LottoPublisher {
#moneyAmount;
setMoneyAmount(moneyAmount) {
this.validate(moneyAmount);
this.#moneyAmount = moneyAmount;
}
validate(moneyAmount) {
if (moneyAmount % 1000 !== 0) {
throw new Error('안돼임마');
}
}
}
async setMoneyAmountFromInput() {
await this.#retryOnFailure(async () => {
const moneyAmount = await this.#view.readMoneyAmount();
this.#lottoPublisher.setMoneyAmount(moneyAmount);
Console.print('잘 됐음');
});
}
async setWinningNumbersFromInput() {
await this.#retryOnFailure(async () => {
const winningNumbers = await this.#view.readWinningNumbers();
this.#lottoService.setWinningNumbers(winningNumbers);
Console.print(this.#lottoService.getWinningNumbers());
});
}
async setBonusNumbersFromInput() {
await this.#retryOnFailure(async () => {
const bonusNumber = await this.#view.readBonusNumber();
this.#lottoService.setBonusNumber(bonusNumber);
Console.print(this.#lottoService.getBonusNumber());
});
}
실행 로그는 아래와 같았다
penfreak@choegwanghuiui-MacBookAir src % node index.js
구입금액을 입력해 주세요.
a
숫자만 입력할 수 있습니다.
구입금액을 입력해 주세요.
10
안돼임마
구입금액을 입력해 주세요.
1000
잘 됐음
당첨 번호를 입력해 주세요.
123
길이오류
당첨 번호를 입력해 주세요.
1,2,3
길이오류
당첨 번호를 입력해 주세요.
1,2,3,4,5,6
[ 1, 2, 3, 4, 5, 6 ]
보너스 번호를 입력해 주세요.
6
보너스번호가 당첨번호랑 중복임
보너스 번호를 입력해 주세요.
7
7
도중에 오류가 발생하면 오류가 난 곳부터 다시 입력받도록 잘 구현됐다!
import { Random } from '@woowacourse/mission-utils';
import NUMBER from '../utils/constants/number.js';
import Lotto from '../Lotto.js';
import CustomError from '../errors/CustomError.js';
const { game } = NUMBER;
const { lotto } = game;
class LottoPublisher {
#moneyAmount;
//...기존의 코드
#generateRandomLottoNumbers() {
return Random.pickUniqueNumbersInRange(
lotto.minNumber,
lotto.maxNumber,
lotto.length,
);
}
#publishSingleLotto() {
const lottoNumbers = this.#generateRandomLottoNumbers();
return new Lotto(lottoNumbers);
}
publishLottos() {
const count = this.#moneyAmount / game.money.unitAmount;
return Array.from({ length: count }, () => this.#publishSingleLotto());
}
}
export default LottoPublisher;
import CustomError from '../errors/CustomError.js';
import LottoResultCalculator from './LottoResultCalculator.js';
class LottoService {
static TICKET_PRICE = 1000;
#winningNumbers;
#bonusNumber;
#lottoTickets;
#resultCalculator = new LottoResultCalculator();
setWinningNumbers(numbers) {
this.validateWinningNumbers(numbers);
this.#winningNumbers = numbers;
}
validateWinningNumbers(numbers) {
if (numbers.length !== 6) {
throw new CustomError('길이오류');
}
if (new Set(numbers).size !== numbers.length) {
throw new CustomError('중복있음');
}
}
setBonusNumber(number) {
this.validateBonusNumber(number);
this.#bonusNumber = number;
}
validateBonusNumber(number) {
if (this.#winningNumbers.includes(number)) {
throw new CustomError('보너스번호가 당첨번호랑 중복임');
}
}
setLottoTickets(tickets) {
this.#lottoTickets = tickets;
}
getWinningNumbers() {
return this.#winningNumbers;
}
getBonusNumber() {
return this.#bonusNumber;
}
getLottoTickets() {
return this.#lottoTickets;
}
}
export default LottoService;
printLottoPurchaseResult(lottos) {
const count = lottos.length;
this.printNewLine();
this.#outputView.print(MessageFormat.lottoPurchaseHeader(count));
const lottoNumbers = lottos.map(item =>
MessageFormat.lottoTicket(item.getNumbers()),
);
lottoNumbers.forEach(item => this.#outputView.print(item));
this.printNewLine();
}
//...
async executeLottoGame() {
await this.setMoneyAmountFromInput();
const tickets = this.#lottoPublisher.publishLottos();
this.#lottoService.setLottoTickets(tickets);
this.#view.printLottoPurchaseResult(tickets);
//...
import MessageFormat from '../utils/MessageFormat.js';
import NUMBER from '../utils/constants/number.js';
const { statistics } = NUMBER;
const { winningCriteria, prizes } = statistics;
class LottoResultCalculator {
#result = {
first: 0,
second: 0,
third: 0,
fourth: 0,
fifth: 0,
};
calculateResults(tickets, winningNumbers, bonusNumber) {
tickets.forEach(ticket => {
this.#updateResult(ticket, winningNumbers, bonusNumber);
});
return this.#result;
}
#updateResult(ticket, winningNumbers, bonusNumber) {
const matchCount = this.#countMatchingNumbers(ticket, winningNumbers);
const hasBonusMatch = ticket.getNumbers().includes(bonusNumber);
this.#incrementPrizeCount(matchCount, hasBonusMatch);
}
#incrementPrizeCount(matchCount, hasBonusMatch) {
const prizeCategory = this.#findPrizeCategory(matchCount, hasBonusMatch);
if (prizeCategory) this.#result[prizeCategory] += 1;
}
#findPrizeCategory(matchCount, hasBonusMatch) {
const winningCriterion = Object.values(winningCriteria).find(
criterion =>
matchCount === criterion.matchCount &&
(!criterion.bonusMatch || hasBonusMatch),
);
return Object.keys(winningCriteria).find(
key => winningCriteria[key] === winningCriterion,
);
}
#countMatchingNumbers(ticket, winningNumbers) {
return ticket.getNumbers().filter(number => winningNumbers.includes(number))
.length;
}
calculateProfitRate(moneyAmount) {
const totalPrizeMoney = this.#calculateTotalPrizeMoney();
const profitRate = (totalPrizeMoney / moneyAmount) * 100;
return profitRate.toFixed(1);
}
#calculateTotalPrizeMoney() {
return Object.entries(this.#result).reduce(
(total, [prizeCategory, count]) => {
const prizeMoney = prizes[prizeCategory] || 0;
return total + prizeMoney * count;
},
0,
);
}
}
export default LottoResultCalculator;
const firstPrizeCriteria = Object.freeze({ matchCount: 6, bonusMatch: false });
const secondPrizeCriteria = Object.freeze({ matchCount: 5, bonusMatch: true });
const thirdPrizeCriteria = Object.freeze({ matchCount: 5, bonusMatch: false });
const fourthPrizeCriteria = Object.freeze({ matchCount: 4, bonusMatch: false });
const fifthPrizeCriteria = Object.freeze({ matchCount: 3, bonusMatch: false });
const winningCriteria = Object.freeze({
first: firstPrizeCriteria,
second: secondPrizeCriteria,
third: thirdPrizeCriteria,
fourth: fourthPrizeCriteria,
fifth: fifthPrizeCriteria,
});
const prizes = Object.freeze({
first: 2000000000,
second: 30000000,
third: 1500000,
fourth: 50000,
fifth: 5000,
});
//Lottoservice.js
calculateResults() {
const tickets = this.getLottoTickets();
const winningNumbers = this.getWinningNumbers();
const bonusNumber = this.getBonusNumber();
return this.#resultCalculator.calculateResults(
tickets,
winningNumbers,
bonusNumber,
);
}
calculateProfitRate() {
const totalSpentMoney =
this.#lottoTickets.length * LottoService.TICKET_PRICE;
const profitRate =
this.#resultCalculator.calculateProfitRate(totalSpentMoney);
return profitRate;
}
async executeLottoGame(){
const { results, profitRate } = this.executeLottoMatch();
this.displayLottoResult(results, profitRate);
}
displayLottoResult(results, profitRate) {
this.#view.printLottoStats(results);
this.#view.printProfitRate(profitRate);
}
printLottoStats(result) {
this.#printLottoStatsHeader();
Object.entries(result).forEach(([category, count]) => {
this.#printLottoStatItem(category, count);
});
this.printNewLine();
}
#printLottoStatsHeader() {
this.printNewLine();
this.#outputView.print(MESSAGE.statsHeader);
}
#printLottoStatItem(category, count) {
const prize = prizes[category];
const resultMessage = MessageFormat.lottoResultMessage(
category,
prize,
count,
);
this.#outputView.print(resultMessage);
}
printProfitRate(profitRate) {
const profitRateMessage = MessageFormat.profitRateMessage(profitRate);
this.#outputView.print(profitRateMessage);
}
{first : 0, second: 0 ...}
을 돌면서 각 카테고리와 갯수를 토대로 MessageFormat의 lottoResultMessage함수를 통해 메세지로 변환해주고, 출력한다.const MessageFormat = {
lottoPurchaseHeader: count => `${count}개를 구매했습니다.`,
lottoTicket: numbers => `[${numbers.join(SYMBOL.lottoNumberSeparator)}]`,
formatPrize: prize => new Intl.NumberFormat('ko-KR').format(prize),
formatProfitRate: profitRate =>
new Intl.NumberFormat('ko-KR', {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
}).format(profitRate),
lottoResultMessage: (category, prize, ticketCount) => {
const { matchCount, bonusMatch } = winningCriteria[category];
return `${matchCount}개 일치${
bonusMatch ? ', 보너스 볼 일치' : ''
} (${MessageFormat.formatPrize(prize)}원) - ${ticketCount}개`;
},
profitRateMessage: profitRate =>
`총 수익률은 ${MessageFormat.formatProfitRate(profitRate)}%입니다.`,
};
lottoResultMessage는 category, prize, ticketCount를 받는다.
winningCriteria의 [category]값을 불러온다. (first, second 등의 당첨기준)
matchCount, bonusMatch로 쪼개고, 몇 개 일치인지 표시하고 해당 순위의 당첨기준에 보너스 번호 일치가 포함되어있다면 '보너스 볼 일치'를 추가한다.
prize를 formatPrize에 넣어 1000 단위로 쉼표로 구분된 문자열로 바꿔준다 (20,000 이렇게)
바꾼 값을 (prize원)으로 표기한다.
ticketCount로 몇 개 당첨됐는지도 표기해준다.
profitRateMessage는 formatProfitRate로 1000단위로 쉼표 구분 + 소수점 첫째자리까지 표기되게끔 처리해준다.
여기까지 구현하고 테스트가 통과하는 걸 확인했다! 중간에 몇 가지 문제가 있었지만 그건 별도의 포스팅에서 다루기로 하고...
일단 지금 시점에서 필요한 건 다음과 같다.
할 건 많다! 시간은 없는데!!!!!