Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4882a47
docs(readme): 구현할 기능 목록 작성
goodsmell Jan 10, 2026
8fa828f
feat(money): 돈 입력 기능 구현
goodsmell Jan 10, 2026
bae0f48
feat(issue): 구매한 만큼 로또 발행 기능 구현
goodsmell Jan 10, 2026
a44b5b6
feat(winning): 당첨번호 입력 기능 구현
goodsmell Jan 10, 2026
2690d52
feat(bonus): 보너스번호 입력 기능 구현
goodsmell Jan 10, 2026
5f2c8ce
feat(statistic): 당첨 통계 계산 및 출력 기능 구현
goodsmell Jan 10, 2026
234ef7b
fix(statistic): 0개 일치 개수를 저장하는 키 값 변경
goodsmell Jan 10, 2026
82e7f50
docs(readme): 예외상황 정리
goodsmell Jan 10, 2026
fd6c11d
docs(readme): 도전 목표 작성
goodsmell Jan 10, 2026
45a3990
refector(constant): 에러메시지 상수화
goodsmell Jan 10, 2026
1afd13f
feat(test): money 클래스 테스트코드 구현
goodsmell Jan 10, 2026
0ae49a2
refector(money): 돈 입력 검증 기능 클래스 분리
goodsmell Jan 10, 2026
0fddfa2
feat(issue): 로또 발급 클래스 테스트 구현
goodsmell Jan 10, 2026
a84d5cf
refector(issue): 로또 발급 클래스 분리
goodsmell Jan 10, 2026
86922d6
chore(issue): 로또 발급 클래스 이름 변경
goodsmell Jan 10, 2026
72c7751
chore(winning): 당첨 번호 클래스 이름 변경
goodsmell Jan 10, 2026
71806cb
feat(winning): 당첨 번호 클래스 테스트 코드 구현
goodsmell Jan 10, 2026
718ae76
feat(bonus): 보너스번호 테스트코드 작성
goodsmell Jan 10, 2026
e7bf8dd
refector(bonus): app에서 보너스 번호 클래스 분리
goodsmell Jan 10, 2026
ae42f73
refectore(statistic): 당첨 통계 계산 클래스 분리
goodsmell Jan 10, 2026
2593723
chore(statistic): 당첨 통계 계산 클래스 이름 변경
goodsmell Jan 10, 2026
395c07c
refector(statistic): 등수 계산 함수 기능 분리
goodsmell Jan 10, 2026
dbe3464
feat(issue): 로또 번호 오름차순 정렬 발급 기능 구현
goodsmell Jan 10, 2026
27d9c18
chore(issue): 로또 발급 테스트 파일 이름 변경
goodsmell Jan 10, 2026
61b5084
refector(all): 동일하게 쓰이는 data 상수화
goodsmell Jan 10, 2026
90b728a
docs(readme): 도전 목표 추가
goodsmell Jan 10, 2026
4f70178
docs(readme): 구현할 기능 목록 및 도전 목표 수정
goodsmell Jan 10, 2026
5e012f2
refector(app): 변수명 변경
goodsmell Jan 10, 2026
6fae26d
refector(app): 변수명 오타 수정
goodsmell Jan 10, 2026
95d1bf6
refector(constant): 에러 메시지 수정
goodsmell Jan 10, 2026
53c22b0
docs(readme): 에러메시지 수정 반영
goodsmell Jan 10, 2026
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# javascript-planetlotto-precourse

# 행성 로또

## 구현할 기능 목록

- 로또를 구매한다
- 로또 1장의 가격은 500원이다.
- 로또를 발행한다
- 로또를 구한 가격만큼 로또를 발행한다.
- 로또 한장에는 1~30 까지 수에서 중복되지 않는 수 5개가 뽑힌다.
- 당첨 번호를 입력한다.
- 1~30 사이의 중복되지 않는 수 5개
- 보너스번호를 입력한다.
- 1~30 사이의 수 1개
- 당첨번호와 중복되지 않는 수
- 당첨 통계를 제공한다
- 당첨 기준

```bash
- 1등: 5개 번호 일치 / 100,000,000원
- 2등: 4개 번호 + 보너스 번호 일치 / 10,000,000원
- 3등: 4개 번호 일치 / 1,500,000원
- 4등: 3개 번호 일치 + 보너스 번호 일치 / 500,000원
- 5등: 2개 번호 일치 + 보너스 번호 일치 / 5,000원
```

- 발행한 로또와 입력한 당첨 번호의 일치 개수를 계산한다.
- 발행한 로또와 입력한 보너스 번호의 존재 여부를 계산한다.
- 당첨 기준에 따라 각 등수에 해당하는 로또의 개수를 계산한다.

## 예외 처리

| 예외상황 | 에러 메시지 |
| ------------------------------------------ | ---------------------------------------------------- |
| 구입 금액이 숫자가 아닌 경우 | [ERROR] 숫자로 입력해주세요 |
| 구입 금액이 500원 단위가 아닌 경우 | [ERROR] 500원 단위로 입력해주세요. |
| 당첨 번호가 5개가 아닌 경우 | [ERROR] 로또 번호는 5개여야 합니다. |
| 당첨 번호가 1~30 사이의 숫자가 아닌 경우 | [ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다. |
| 당첨 번호가 중복된 경우 | [ERROR] 중복 숫자가 포함되어 있습니다. |
| 보너스 번호가 1~30 사이의 숫자가 아닌 경우 | [ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다. |
| 보너스 번호가 당첨번호와 중복된 경우 | [ERROR] 이미 입력된 숫자입니다 |

## 도전 과제

### 목표 : 주어진 시간 안에 코드 품질을 높히자!

- 모든 함수는 15줄을 넘지 않는다.
- 상수는 변수화 한다.
- 함수/변수 이름에 의미를 정확하게 포함한다.
- tdd를 적용하여 리펙토링 한다.
- mvc 구조로 분리한다.
17 changes: 17 additions & 0 deletions __tests__/BonusNumberTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import BonusNumber from '../src/model/BonusNumber.js';
import { ERROR_MESSAGE } from '../src/constants/message.js';

describe('보너스 번호 클래스 테스트', () => {
const winningNums = [1, 2, 3, 4, 5, 6];

test('1~30 사이의 수가 아니면 예외가 발생한다.', () => {
expect(() => BonusNumber.validate(31, winningNums)).toThrow(
ERROR_MESSAGE.INCORRECT_NUMBER_OF_RANGES,
);
});
test('당첨번호와 중복이면 예외가 발생한다.', () => {
expect(() => BonusNumber.validate(1, winningNums)).toThrow(
ERROR_MESSAGE.DUPLICATE_NUMBER_WITH_WINNING,
);
});
});
17 changes: 17 additions & 0 deletions __tests__/InputMoneyTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Money from '../src/model/Money.js';
import { ERROR_MESSAGE } from '../src/constants/message.js';

describe('Money 클래스 테스트', () => {
test.each([
['500j', '영어가 입력된 경우'],
['', '아무것도 입력되지 않은 경우'],
])('숫자 외의 것이 입력되면 예외가 발생한다. (%s)', (moneys) => {
expect(() => new Money(moneys)).toThrow(ERROR_MESSAGE.NOT_NUMBER);
});
test.each([
[100, '100원이 입력된 경우'],
[510, '510원이 입력된 경우'],
])('500원 단위의 입력이 아니면 예외가 발생한다(%s)', (moneys) => {
expect(() => new Money(moneys)).toThrow(ERROR_MESSAGE.NOT_UNITS_500_WON);
});
});
15 changes: 15 additions & 0 deletions __tests__/LottoIssueMachineTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import LottoIssueMachine from '../src/model/LottoIssueMachine.js';

describe('로또 발급 클래스 테스트', () => {
const money = 5000;
const lottoCount = 10;
test('입력받은 돈의 로또 발행 개수를 계산한다.', () => {
expect(LottoIssueMachine.calculatorLottoCount(money)).toBe(lottoCount);
});

const issuedLotto = LottoIssueMachine.issue(money);

test('로또 발행 개수 만큼 로또를 생성한다', () => {
expect(issuedLotto.length).toBe(lottoCount);
});
});
23 changes: 23 additions & 0 deletions __tests__/WinningNumbersTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import WinningNumbers from '../src/model/WinningNumbers.js';
import { ERROR_MESSAGE } from '../src/constants/message.js';

describe('당첨 번호 클래스 테스트', () => {
test.each([
[[1, 2, 3, 4, 5, 6], '6개인 경우'],
[[1, 2, 3, 4], '4개인 경우'],
])('로또 번호의 개수가 5개가 아니면 예외가 발생한다. (%s)', (numbers) => {
expect(() => new WinningNumbers(numbers)).toThrow(ERROR_MESSAGE.WRONG_WINNING_COUNT);
});

test.each([
[[1, 2, 3, 4, 31], '31이 포함된 경우'],
[[1, 2, 3, 4, 0], '0이 포함된 경우'],
])('로또 번호의 범위가 1~30이 아니면 예외가 발생한다.. (%s)', (numbers) => {
expect(() => new WinningNumbers(numbers)).toThrow(ERROR_MESSAGE.INCORRECT_NUMBER_OF_RANGES);
});
test('로또 번호에 중복된 숫자가 있으면 예외가 발생한다.', () => {
expect(() => {
new WinningNumbers([1, 2, 3, 5, 5]);
}).toThrow(ERROR_MESSAGE.DUPLICATE_NUMBER);
});
});
68 changes: 67 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,71 @@
import { InputView, OutputView } from './view.js';
import LottoIssueMachine from './model/LottoIssueMachine.js';
import WinningNumbers from './model/WinningNumbers.js';
import BonusNumber from './model/BonusNumber.js';
import WinningsStatisticsCalculator from './model/WinningsStatisticsCalculator.js';
import Money from './model/Money.js';

class App {
async run() {}
async run() {
const money = await this.getMoney();
const issueLottos = LottoIssueMachine.issue(money);
this.printIssuedLottos(issueLottos);

const winningNumbers = await this.getWinnerNumbers();
const bonusNumber = await this.getBonusNumbers(winningNumbers);

const lottoCalculation = new WinningsStatisticsCalculator(
issueLottos,
winningNumbers,
bonusNumber,
);
this.printStatistics(lottoCalculation.getCountRank());

return;
}

async getMoney() {
return this.#retry(async () => {
const inputMoney = await InputView.askAmount();
const money = new Money(inputMoney);
return money.getMoney();
});
}

async getWinnerNumbers() {
return this.#retry(async () => {
const numbers = await InputView.askWinningLotto();
const winnerLotto = new WinningNumbers(numbers);

return winnerLotto.getLottos();
});
}

async getBonusNumbers(winning) {
return this.#retry(async () => {
const bonus = await InputView.askBonusNumber();
BonusNumber.validate(bonus, winning);
return bonus;
});
}

printIssuedLottos(lottos) {
OutputView.printPurchasedLottos(lottos);
}

printStatistics(countRank) {
OutputView.printResult(countRank);
}

async #retry(task) {
while (true) {
try {
return await task();
} catch (e) {
OutputView.printErrorMessage(e.message);
}
}
}
}

export default App;
5 changes: 5 additions & 0 deletions src/constants/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const data = Object.freeze({
MONEY_UNIT: 500,
MAX_NUM: 30,
MIN_NUM: 1,
});
8 changes: 8 additions & 0 deletions src/constants/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const ERROR_MESSAGE = Object.freeze({
NOT_NUMBER: '숫자로 입력해주세요.',
NOT_UNITS_500_WON: `500원 단위로 입력해주세요.`,
WRONG_WINNING_COUNT: '로또 번호는 5개여야 합니다.',
INCORRECT_NUMBER_OF_RANGES: '로또 번호는 1부터 30 사이의 숫자여야 합니다.',
DUPLICATE_NUMBER: '중복 숫자가 포함되어 있습니다.',
DUPLICATE_NUMBER_WITH_WINNING: '이미 입력된 숫자입니다.',
});
14 changes: 14 additions & 0 deletions src/model/BonusNumber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ERROR_MESSAGE } from '../constants/message.js';
import { data } from '../constants/data.js';
class BonusNumber {
static validate(bonus, winning) {
if (bonus < data.MIN_NUM || bonus > data.MAX_NUM) {
throw new Error(ERROR_MESSAGE.INCORRECT_NUMBER_OF_RANGES);
}
if (winning.includes(bonus)) {
throw new Error(ERROR_MESSAGE.DUPLICATE_NUMBER_WITH_WINNING);
}
}
}

export default BonusNumber;
24 changes: 24 additions & 0 deletions src/model/LottoIssueMachine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MissionUtils } from '@woowacourse/mission-utils';
import { data } from '../constants/data';

class LottoIssueMachine {
static issue(money) {
const count = this.calculatorLottoCount(money);
const lottos = [];

for (let i = 1; i <= count; i++) {
lottos.push(MissionUtils.Random.pickUniqueNumbersInRange(1, 30, 5));
}

lottos.forEach((lotto) => {
lotto.sort((a, b) => a - b);
});

return lottos;
}
static calculatorLottoCount(money) {
return money / data.MONEY_UNIT;
}
}

export default LottoIssueMachine;
26 changes: 26 additions & 0 deletions src/model/Money.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ERROR_MESSAGE } from '../constants/message.js';
import { data } from '../constants/data.js';
class Money {
#money;

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

#validate(money) {
const reg = /^[1-9]\d*$/;
if (!reg.test(money)) {
throw new Error(ERROR_MESSAGE.NOT_NUMBER);
}
if (money % data.MONEY_UNIT !== 0) {
throw new Error(ERROR_MESSAGE.NOT_UNITS_500_WON);
}
}

getMoney() {
return this.#money;
}
}

export default Money;
31 changes: 31 additions & 0 deletions src/model/WinningNumbers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ERROR_MESSAGE } from '../constants/message.js';
import { data } from '../constants/data.js';
class WinningNumbers {
#numbers;

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

#validate(numbers) {
if (numbers.length !== 5) {
throw new Error(ERROR_MESSAGE.WRONG_WINNING_COUNT);
}

if (numbers.some((num) => num < data.MIN_NUM || num > data.MAX_NUM)) {
throw new Error(ERROR_MESSAGE.INCORRECT_NUMBER_OF_RANGES);
}

const checkingDuplicates = new Set(numbers);
if (checkingDuplicates.size !== numbers.length) {
throw new Error(ERROR_MESSAGE.DUPLICATE_NUMBER);
}
}

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

export default WinningNumbers;
54 changes: 54 additions & 0 deletions src/model/WinningsStatisticsCalculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
class WinningsStatisticsCalculator {
countRank = new Map([
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
[0, 0],
]);
matchingMap;
lottos;

constructor(issudeLottos, winningNumbers, bonusNumber) {
this.lottos = { issudeLottos, winningNumbers, bonusNumber };
this.CalculateNumberOfWinning();
this.CalculateRank();
}

CalculateNumberOfWinning() {
const matchingMap = this.lottos.issudeLottos.map((lotto) => {
const commonElements = lotto.filter((item) => this.lottos.winningNumbers.includes(item));
const matchingWinning = new Set(commonElements).size;

const matchingBonus = lotto.includes(this.lottos.bonusNumber);

return { winning: matchingWinning, hasBonus: matchingBonus };
});

this.matchingMap = matchingMap;
}

CalculateRank() {
this.matchingMap.forEach((result) => {
if (result.winning === 5) this.setRank(1);
if (result.winning === 4 && result.hasBonus) this.setRank(2);
if (result.winning === 4) this.setRank(3);
if (result.winning === 3 && result.hasBonus) this.setRank(4);
if (result.winning === 2 && result.hasBonus) this.setRank(5);
if (result.winning === 0) this.setRank(0);
});
}

setRank(rank) {
let currentScore = this.countRank.get(rank);
this.countRank.set(rank, currentScore + 1);
return;
}

getCountRank() {
return this.countRank;
}
}

export default WinningsStatisticsCalculator;