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

## 기능 목록

- [x] 구입 금액에 해당하는 만큼 발행할 로또 수량을 정한다.
- [x] 로또 수량만큼 중복되지 않은 5개의 숫자로 구성된 로또를 발행한다.
- [ ] 당첨 번호와 발행된 로또를 비교한다.
- [ ] 4개의 번호가 일치하면 보너스 번호와 비교한다.
- [ ] 비교된 결과값을 바탕으로 당첨 내역을 계산한다.
- [ ] 사용자가 잘못된 값을 입력하면 에러 메시지와 Error를 발생시키고 재입력을 받는다.
15 changes: 15 additions & 0 deletions __tests__/LottoMachineTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import LottoMachine from "../src/model/LottoMachine.js";

describe("LottoMachine 클래스 테스트", () => {
test("로또 가격만큼 나누어 떨어지지 않으면 에러를 발생한다.", () => {
const purchase = 600;
expect(() => {
new LottoMachine({ purchase });
}).toThrow("INVALID_PURCHASE");
});

test("로또 가격만큼 발행할 로또 수량을 계산한다.", () => {
const purchase = 1000;
expect(new LottoMachine({ purchase }).getQuantity()).toBe(2);
});
});
31 changes: 31 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Lotto from "../src/model/Lotto.js";

describe("Lotto 클래스 테스트", () => {
test("중복된 숫자가 있으면 에러를 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 4]);
}).toThrow("NUMBERS_DUPLICATION");
});

test("1 ~ 30 사이의 숫자 범위에 들지 않으면 에러를 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 31]);
}).toThrow("NUMBERIC_RANG");
});

test("로또를 오름차순으로 반환한다.", () => {
expect(new Lotto([30, 1, 5, 3, 22]).getNumbers()).toEqual([
1, 3, 5, 22, 30,
]);
});

test("비교할 로또와 같은 숫자들을 가지면 true를 반환한다.", () => {
expect(
new Lotto([1, 4, 8, 29, 10]).isEqual([1, 4, 8, 10, 29])
).toBeTruthy();
});

test("비교할 로또와 다른 숫자들을 가지면 false를 반환한다.", () => {
expect(new Lotto([1, 4, 7, 10, 29]).isEqual([1, 4, 7, 10, 5])).toBeFalsy();
});
});
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { InputView } from "./view.js";
import { OutputView } from "./view.js";
import LottoController from "./application/LottoController.js";
import LottoService from "./application/LottoService.js";

class App {
async run() {}
async run() {
const lottoController = new LottoController({
InputView,
OutputView,
LottoService,
});
await lottoController.run();
}
}

export default App;
52 changes: 52 additions & 0 deletions src/application/LottoController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DomainError, InvalidWinningError } from "../error/ApplicationError.js";

export default class LottoController {
#inputView;
#outputView;
#LottoService;

constructor({ InputView, OutputView, LottoService } = {}) {
this.#inputView = InputView;
this.#outputView = OutputView;
this.#LottoService = LottoService;
}

async #handlingError(fn) {
while (true) {
try {
return await fn();
} catch (error) {
const message = `[ERROR] ${error.message}`;

this.#outputView.printErrorMessage(message);

if (error instanceof DomainError) continue;
if (error instanceof InvalidWinningError) this.#askWinningLotto();

throw error;
}
}
}

async #askWinningLotto() {
return await this.#handlingError(() => this.#inputView.askWinningLotto());
}

async run() {
const amount = await this.#handlingError(() => this.#inputView.askAmount());
const winningLotto = this.#askWinningLotto();
const bonus = await this.#handlingError(() =>
this.#inputView.askBonusNumber()
);

const service = await this.#handlingError(() => this.#LottoService());
const { issueLottos, rankingCounts } = service.run(
amount,
winningLotto,
bonus
);

this.#outputView.printPurchasedLottos(issueLottos);
this.#outputView.printResult(rankingCounts);
}
}
31 changes: 31 additions & 0 deletions src/application/LottoService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import LottoMachine from "../model/LottoMachine.js";
import LottoWinning from "../model/LottoWinning.js";
import LottoWinningRank from "../model/LottoWinningRank.js";
import WinnersStatistics from "../model/WinnersStatistics.js";

export default class LottoService {
constructor() {}

run(purchase, winningNumbers, bonus) {
const lottoMachine = new LottoMachine(purchase);
const issueLottos = lottoMachine.issue();

const lottoWinning = new LottoWinning({
winningNumbers,
bonus,
lottoWinningRank: LottoWinningRank,
});

const winningRankings = issueLottos.map((lotto) =>
lottoWinning.evaluate(lotto.getNumbers())
);

const winnersStatistics = new WinnersStatistics(winningRankings);

const rankings = LottoWinningRank.getRanks();

const rankingCounts = winnersStatistics.getStatistics(rankings);

return { issueLottos, rankingCounts };
}
}
7 changes: 7 additions & 0 deletions src/constants/errorMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const ERROR_TYPES = {
NUMBERS_DUPLICATION: "중복되는 숫자가 있습니다.",
NUMBERIC_RANG: "숫자 범위에서 벗어났습니다.",
INVALID_PURCHASE: "로또 가격에 해당하는 가격을 입력해주세요.",
};

export default ERROR_TYPES;
17 changes: 17 additions & 0 deletions src/error/ApplicationError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class ApplicationError extends Error {
constructor(message) {
super(message);
}
}

export class DomainError extends ApplicationError {
constructor(message) {
super(`${message}`);
}
}

export class InvalidWinningError extends ApplicationError {
constructor(message) {
super(`${message}`);
}
}
28 changes: 28 additions & 0 deletions src/model/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { DomainError } from "../error/ApplicationError.js";
import ERROR_TYPES from "../constants/errorMessage.js";

export default class Lotto {
#numbers;

constructor(numbers) {
this.#validateNumbers(numbers);

this.#numbers = numbers;
}

#validateNumbers(numbers) {
if (new Set(numbers).size !== numbers.length)
throw new DomainError(ERROR_TYPES.NUMBERS_DUPLICATION);

const isNumberRang = numbers.every((number) => 1 <= number && number <= 30);
if (!isNumberRang) throw new DomainError(ERROR_TYPES.NUMBERIC_RANG);
}

getNumbers() {
return this.#numbers.sort((a, b) => a - b);
}

isEqual(compareLotto) {
return this.#numbers.every((number) => compareLotto.includes(number));
}
}
45 changes: 45 additions & 0 deletions src/model/LottoMachine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import randomPickUniqueNumbers from "../utils/randompickUniqueNumbers.js";
import LottoFactory from "../utils/LottoFactory.js";

import { DomainError } from "../error/ApplicationError.js";
import ERROR_TYPES from "../constants/errorMessage.js";

export default class LottoMachine {
#quantity;
#lottoFactory;
#randomFn;

constructor({
purchase,
lottoFactory = LottoFactory,
randomFn = randomPickUniqueNumbers,
} = {}) {
this.#validatePurchase(purchase);

this.#quantity = purchase / 500;
this.#lottoFactory = lottoFactory;
this.#randomFn = randomFn;
}

#validatePurchase(purchase) {
if (purchase % 500 !== 0)
throw new DomainError(ERROR_TYPES.INVALID_PURCHASE);
}

#checkAvailable(issueLottos, newLotto) {
return issueLottos.every((lotto) => lotto.isEqual(newLotto));
}

issue() {
const issueLottos = [];

while (issueLottos.length < this.#quantity) {
const newLotto = this.#lottoFactory.createLotto(this.#randomFn());
const isAvailable = this.#checkAvailable(issueLottos, newLotto);

if (isAvailable) issueLottos.push(newLotto);
}

return issueLottos;
}
}
53 changes: 53 additions & 0 deletions src/model/LottoWinning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import ERROR_TYPES from "../constants/errorMessage.js";
import { DomainError, InvalidWinningError } from "../error/ApplicationError.js";

export default class LottoWinning {
#winningNumbers;
#bonus;
#lottoWinningRank;

constructor({ winningNumbers, bonus, lottoWinningRank } = {}) {
this.#validateBonus(bonus);

this.#winningNumbers = winningNumbers;
this.#bonus = bonus;
this.#lottoWinningRank = lottoWinningRank;
}

validateWinningNumbers(winningNumbers) {
if (new Set(winningNumbers).size !== winningNumbers.length)
throw new InvalidWinningError(ERROR_TYPES.NUMBERS_DUPLICATION);

const isNumberRang = winningNumbers.every(
(number) => 1 <= number && number <= 30
);
if (!isNumberRang) throw new InvalidWinningError(ERROR_TYPES.NUMBERIC_RANG);
}

#validateBonus(bonus) {
const isNumberRange = 1 <= bonus && bonus <= 30;
if (!isNumberRange)
throw new DomainError("1 ~ 30 사이의 숫자 범위에 해당하지 않습니다.");

if (this.#winningNumbers.includes(bonus)) {
throw new DomainError("당첨 번호에 중복되는 보너스 번호입니다.");
}
}

#matchCount(lottoNumbers) {
return lottoNumbers.filter((number) =>
this.#winningNumbers.includes(number)
);
}

#matchBonus(lottoNumbers, matchCount) {
return matchCount === 5 && lottoNumbers.includes(this.#bonus);
}

evaluate(lottoNumbers) {
const matchCount = this.#matchCount(lottoNumbers);
const isMatchBonus = this.#matchBonus(lottoNumbers, matchCount);

return this.#lottoWinningRank.evaluate(matchCount, isMatchBonus);
}
}
56 changes: 56 additions & 0 deletions src/model/LottoWinningRank.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export default class LottoWinningRank {
#rank;
#matchCount;
#isMatchBonus;
#prize;

constructor(rank, matchCount, isMatchBonus, prize) {
this.#rank = rank;
this.#matchCount = matchCount;
this.#isMatchBonus = isMatchBonus;
this.#prize = prize;
}

getRank() {
return this.#rank;
}

getMatchCount() {
return this.#matchCount;
}

getIsMatchBonus() {
return this.#isMatchBonus;
}

getPrize() {
return this.#prize;
}

getRanks() {
return [
LottoWinningRank.FIRST.getRank(),
LottoWinningRank.SECOND.getRank(),
LottoWinningRank.THIRD.getRank(),
LottoWinningRank.FOURTH.getRank(),
LottoWinningRank.FIFTH.getRank(),
LottoWinningRank.NONE.getRank(),
];
}

static FIRST = new LottoWinningRank(1, 5, false, 100000000);
static SECOND = new LottoWinningRank(2, 4, true, 10000000);
static THIRD = new LottoWinningRank(3, 4, false, 1500000);
static FOURTH = new LottoWinningRank(4, 3, true, 500000);
static FIFTH = new LottoWinningRank(5, 2, true, 5000);
static NONE = new LottoWinningRank(0, 0, false, 0);

static of(matchCount, isMatchBonus) {
if (matchCount === 5) return LottoWinningRank.FIRST;
if (matchCount === 4 && isMatchBonus) return LottoWinningRank.SECOND;
if (matchCount === 4) return LottoWinningRank.THIRD;
if (matchCount === 3 && isMatchBonus) return LottoWinningRank.FOURTH;
if (matchCount === 2 && isMatchBonus) return LottoWinningRank.FIFTH;
return LottoWinningRank.NONE;
}
}
Loading