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

## 최종 테스트 - <🌏 행성 로또 🎱> 기능 구현 목록

### 입력 처리

- [x] 사용자로부터 로또 구입 금액 입력받기 (`askAmount()` 사용)
- [x] return -> number
- [x] 사용자로부터 당첨 번호 입력받기 (`askWinningLotto()` 사용)
- [x] return -> number[]
- [x] Lotto class 만들어서 사용 (멤버 => numbers)
- [x] 사용자로부터 보너스 번호 입력받기 (`askBonusNumber()` 사용)
- [x] return -> number

### 결과 계산

- [x] 입력 받은 로또 구입 금액 / 500 계산하여 로또 수량 도출
- [x] 각 발행한 로또에 중복되지 않은 5개의 정수(1부터 30 사이) 부여 (`MissionUtils.Random.pickUniqueNumbersInRange(1, 30, 5)` 사용)
- [x] Lotto class instance 생성
- [x] 로또 번호 오름차순 정렬
- [x] 입력 받은 당첨 번호와 발행된 로또의 5개 정수를 비교
- [x] 5개 모두 일치하지 않고 4개만 일치했을 경우 발행된 로또의 4개 정수와 보너스 번호를 비교
- [x] 4개 모두 일치하지 않고 3개만 일치했을 경우 발행된 로또의 3개 정수와 보너스 번호를 비교
- [x] 3개 모두 일치하지 않고 3개만 일치했을 경우 발행된 로또의 2개 정수와 보너스 번호를 비교
- [x] 당첨 내역 계산
- [x] 1등부터 6등 각각의 개수

### 예외 처리

- [x] Error 발생 시 `process.exit()`을 호출하지 않고 `[ERROR]` 메시지와 함께 Error throw
- [x] 'OutputView의 `printErrorMessage(message)` 사용'
- [x] 구입 금액 입력
- [x] 빈 문자열 입력 시
- [x] 숫자가 아닌 값 입력 시
- [x] 500 미만 값 입력 시
- [x] 500으로 나눴을 때 정수형 숫자로 나눠떨어지지 않는 경우
- [x] 당첨 번호 입력
- [x] 1부터 30 사이 외의 로또 번호 입력 시
- [x] 정수가 아닌 값 입력 시
- [x] 5개 숫자가 아닌 갯수 입력 시 (빈 문자열도 같이 검증 역할)
- [x] 중복된 번호 포함 시
- [x] 보너스 번호 입력
- [x] 1부터 30 사이 외의 로또 번호 입력 시
- [x] 빈 문자열 입력 시
- [x] 정수가 아닌 값 입력 시
- [x] 당첨 번호와 중복 시

### 출력

- [x] 발행한 로또 수량 출력 (`printPurchasedLottos(lottos)` 사용)
- [x] lottos -> `number[][]` e.g. numbers[[1,2,3,4,5],[6,7,8,9,10]]
- [x] 당첨 내역 출력 (`printResult(countsByRank)` 사용)
- [x] countsByRank -> `Map<number, number>` e.g. [ 1, firstCount ], [ 2, secondCount ]
15 changes: 15 additions & 0 deletions __tests__/LottoFactoryTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import LottoFactory from '../src/model/LottoFactory.js';

describe('로또 팩토리 테스트', () => {
test('로또를 구매한 개수만큼만 생성한다.', () => {
const lottos = LottoFactory.createLotto(5);

expect(lottos).toHaveLength(5);
});

test('정상적으로 에러없이 로또가 생성된다.', () => {
expect(() => {
LottoFactory.createLotto(5);
}).not.toThrow();
});
});
118 changes: 118 additions & 0 deletions __tests__/LottoStatisticsTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import LottoStatistics from '../src/model/LottoStatistics.js';
import Lotto from '../src/model/Lotto.js';

describe('LottoStatistics 테스트(calculateRankResults Map 테스트)', () => {
test('당첨된 로또가 없으면 꽝이 2개이다. (2개 구매시)', () => {
const lottos = [new Lotto([1, 2, 3, 4, 5]), new Lotto([7, 8, 9, 10, 11])];
const winningLotto = new Lotto([13, 14, 15, 16, 17]);
const bonusNumber = 19;

const result = LottoStatistics.calculateRankResults(lottos, winningLotto, bonusNumber);

const expectedMap = new Map([
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
[0, 2],
]);

expect(result).toEqual(expectedMap);
});

test('1등이 1개 있으면 1등 개수가 1이고 꽝 개수가 1개이다. (2개 구매시)', () => {
const lottos = [new Lotto([1, 2, 3, 4, 5]), new Lotto([7, 8, 9, 10, 11])];
const winningLotto = new Lotto([1, 2, 3, 4, 5]);
const bonusNumber = 7;

const result = LottoStatistics.calculateRankResults(lottos, winningLotto, bonusNumber);

const expectedMap = new Map([
[1, 1],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
[0, 1],
]);

expect(result).toEqual(expectedMap);
});

test('2등이 1개 있으면 2등 개수가 1이고 꽝 개수가 1개이다. (2개 구매시)', () => {
const lottos = [new Lotto([1, 2, 3, 4, 5]), new Lotto([8, 9, 10, 11, 12])];
const winningLotto = new Lotto([1, 2, 3, 4, 6]);
const bonusNumber = 5;

const result = LottoStatistics.calculateRankResults(lottos, winningLotto, bonusNumber);

const expectedMap = new Map([
[1, 0],
[2, 1],
[3, 0],
[4, 0],
[5, 0],
[0, 1],
]);

expect(result).toEqual(expectedMap);
});

test('3등이 1개 있으면 3등 개수가 1이고 꽝 개수가 1개이다. (2개 구매시)', () => {
const lottos = [new Lotto([1, 2, 3, 4, 5]), new Lotto([9, 10, 11, 12, 13])];
const winningLotto = new Lotto([1, 2, 3, 4, 8]);
const bonusNumber = 7;

const result = LottoStatistics.calculateRankResults(lottos, winningLotto, bonusNumber);

const expectedMap = new Map([
[1, 0],
[2, 0],
[3, 1],
[4, 0],
[5, 0],
[0, 1],
]);

expect(result).toEqual(expectedMap);
});

test('4등이 1개 있으면 4등 개수가 1이고 꽝 개수가 1개이다. (2개 구매시)', () => {
const lottos = [new Lotto([1, 2, 3, 4, 8]), new Lotto([10, 11, 12, 13, 14])];
const winningLotto = new Lotto([1, 2, 3, 6, 7]);
const bonusNumber = 4;

const result = LottoStatistics.calculateRankResults(lottos, winningLotto, bonusNumber);

const expectedMap = new Map([
[1, 0],
[2, 0],
[3, 0],
[4, 1],
[5, 0],
[0, 1],
]);

expect(result).toEqual(expectedMap);
});

test('5등이 1개 있고 4등이 1개 있으면 5등, 4등 개수가 각각 1이다. (2개 구매시)', () => {
const lottos = [new Lotto([1, 2, 3, 8, 9]), new Lotto([1, 2, 8, 20, 25])];
const winningLotto = new Lotto([1, 2, 3, 4, 5]);
const bonusNumber = 8;

const result = LottoStatistics.calculateRankResults(lottos, winningLotto, bonusNumber);

const expectedMap = new Map([
[1, 0],
[2, 0],
[3, 0],
[4, 1],
[5, 1],
[0, 0],
]);

expect(result).toEqual(expectedMap);
});
});
7 changes: 6 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import LottoController from './controller/LottoController.js';

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

export default App;
6 changes: 6 additions & 0 deletions src/constants/lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const LOTTO = {
PRICE: 500,
NUMBER_COUNT: 5,
MIN_NUMBER: 1,
MAX_NUMBER: 30,
};
9 changes: 9 additions & 0 deletions src/constants/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const ERROR_MESSAGES = {
INVALID_LOTTO_COUNT: '로또 번호는 5개여야 합니다.',
DUPLICATE_LOTTO_NUMBERS: '로또 번호에 중복된 숫자가 있습니다.',
LOTTO_NUMBER_OUT_OF_RANGE: '로또 번호에 1부터 30 사이가 아닌 값이 있습니다.',
BELOW_MINIMUM_PURCHASE: '500 미만 값이 입력되었습니다.',
INVALID_PURCHASE_UNIT: '입력값이 500으로 나눴을 때 정수형 숫자로 나눠떨어지지 않습니다.',
BONUS_NUMBER_OUT_OF_RANGE: '보너스 번호에 1부터 30 사이가 아닌 값이 있습니다.',
BONUS_NUMBER_DUPLICATE: '보너스 번호가 당첨 번호와 중복됩니다.',
};
7 changes: 7 additions & 0 deletions src/constants/ranks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const RANKS = [
{ rank: 1, matchingCount: 5, hasBonusNumber: false, winningAmount: 100000000 },
{ rank: 2, matchingCount: 4, hasBonusNumber: true, winningAmount: 10000000 },
{ rank: 3, matchingCount: 4, hasBonusNumber: false, winningAmount: 1500000 },
{ rank: 4, matchingCount: 3, hasBonusNumber: true, winningAmount: 500000 },
{ rank: 5, matchingCount: 2, hasBonusNumber: true, winningAmount: 5000 },
];
82 changes: 82 additions & 0 deletions src/controller/LottoController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { InputView, OutputView } from '../view/view.js';
import { validatePurchaseAmount, validateBonusNumber } from '../utils/validator.js';
import { getLottoQuantity } from '../service/LottoQuantityCalculator.js';
import LottoFactory from '../model/LottoFactory.js';
import LottoStatistics from '../model/LottoStatistics.js';
import Lotto from '../model/Lotto.js';
import { newLinePrinter } from '../utils/newline_printer.js';

class LottoController {
async run() {
// 로또 금액 입력 받기
let inputErrorFlag = true;
let lottoPurchaseAmount = 0;
while (inputErrorFlag) {
try {
lottoPurchaseAmount = await InputView.askAmount();
validatePurchaseAmount(lottoPurchaseAmount);
inputErrorFlag = false;
} catch (error) {
OutputView.printErrorMessage(error.message);
}
}

// 로또 갯수 도출하기
const lottoQuantity = getLottoQuantity(lottoPurchaseAmount);

// 로또 배열 생성하기(팩토리 패턴)
const lottos = LottoFactory.createLotto(lottoQuantity);

// 구매한 로또 출력
newLinePrinter();
OutputView.printPurchasedLottos(lottos);
newLinePrinter();

// 당첨 번호 입력
inputErrorFlag = true;
const winningLotto = [];
while (inputErrorFlag) {
try {
const winningNumbers = await InputView.askWinningLotto();

winningLotto.push(new Lotto(winningNumbers.sort((a, b) => a - b)));
inputErrorFlag = false;
} catch (error) {
OutputView.printErrorMessage(error.message);
}
}

// 보너스 번호 입력
inputErrorFlag = true;
let bonusNumber = 0;

while (inputErrorFlag) {
try {
bonusNumber = await InputView.askBonusNumber();
validateBonusNumber(bonusNumber, winningLotto[0]);

inputErrorFlag = false;
} catch (error) {
OutputView.printErrorMessage(error.message);
}
}

// 구매한 로또 인스턴스 생성 후 로또 랭크 갯수 도출
const lottoInstances = [];
lottos.forEach((lotto) => {
lottoInstances.push(new Lotto(lotto));
});

const countsByRank = LottoStatistics.calculateRankResults(
lottoInstances,
winningLotto[0],
bonusNumber,
);

// 로또 당첨 통계 출력
newLinePrinter();
OutputView.printResult(countsByRank);
}
}

export default LottoController;
45 changes: 45 additions & 0 deletions src/model/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { LOTTO } from '../constants/lotto.js';
import { ERROR_MESSAGES } from '../constants/messages.js';

class Lotto {
#numbers;

constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
}

#validate(numbers) {
if (numbers.length !== LOTTO.NUMBER_COUNT) {
throw new Error(ERROR_MESSAGES.INVALID_LOTTO_COUNT);
}

const uniqueArr = new Set(numbers);
if (numbers.length !== uniqueArr.size) {
throw new Error(ERROR_MESSAGES.DUPLICATE_LOTTO_NUMBERS);
}

if (!numbers.every((number) => number <= LOTTO.MAX_NUMBER && number >= LOTTO.MIN_NUMBER)) {
throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE);
}
}

getMatchingCount(winningLotto) {
const winningNumbers = winningLotto.getNumbers();
const matchingCount = winningNumbers.filter((winningNumber) =>
this.#numbers.includes(winningNumber),
);

return matchingCount.length;
}

getNumbers() {
return this.#numbers;
}

hasBonusNumber(bonusNumber) {
return this.#numbers.includes(bonusNumber);
}
}

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

class LottoFactory {
static createLotto(lottoQuantity) {
const lottoArray = Array.from({ length: lottoQuantity }, () => {
const numbers = Random.pickUniqueNumbersInRange(1, 30, 5);
return numbers.sort((a, b) => a - b);
});

return lottoArray;
}
}

export default LottoFactory;
Loading