Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ca777cc
docs: 기능 목록 작성
gustn99 Jan 10, 2026
59d227b
feat: 구매 금액 입력 로직 구현
gustn99 Jan 10, 2026
0908d45
docs: 기능 목록에 로또 번호 조건 추가
gustn99 Jan 10, 2026
63becc5
feat: 로또 발행 로직 구현
gustn99 Jan 10, 2026
ded1cbe
feat: 발행된 로또 출력 로직 구현
gustn99 Jan 10, 2026
106b3d1
refactor: Lotto/LottoNumber 파일 분리
gustn99 Jan 10, 2026
8a3387f
refactor: 임의로 추가한 view 예외 prefix 제거
gustn99 Jan 10, 2026
b3c87f1
refactor: 도메인 객체 내 예외 prefix 제거
gustn99 Jan 10, 2026
3add0e9
feat: 당첨 번호 입력 로직 구현
gustn99 Jan 10, 2026
006f397
feat: 보너스 번호 입력 로직 구현
gustn99 Jan 10, 2026
5015981
feat: 당첨번호 비교 로직 구현
gustn99 Jan 10, 2026
c5a6605
feat: 당첨 통계 출력 로직 구현
gustn99 Jan 10, 2026
2ef9167
fix(LottoResult): 0개 일치를 수합하지 못하는 문제 해결
gustn99 Jan 10, 2026
f82ec5b
feat: 입력 재시도 로직 구현
gustn99 Jan 10, 2026
c10dda2
refactor: 빌더 패턴을 도입해
gustn99 Jan 10, 2026
0c2bc40
refactor: 불필요한 Validator 클래스 제거
gustn99 Jan 10, 2026
c157191
refactor(LottoResult): 순위별 해당 개수를 알려주는 자체 get 메서드 생성
gustn99 Jan 10, 2026
7e49fbf
refactor: builder 사용 방식 변경
gustn99 Jan 10, 2026
05b469e
refactor: 로또 구매 책임을 LottoStore로 분리
gustn99 Jan 10, 2026
2a747a2
refactor: 매직 넘버 상수화
gustn99 Jan 10, 2026
00efe31
docs: 도전 과제 내용 작성
gustn99 Jan 10, 2026
d2c3917
test: Lotto 클래스 테스트 추가
gustn99 Jan 10, 2026
18e9f93
test: LottoNumber 클래스 테스트 추가
gustn99 Jan 10, 2026
fbd9cb4
refactor: 필드 상수를 전역 상수로 변경
gustn99 Jan 10, 2026
55f9c5d
refactor: 로또 하나 당 숫자 개수 상수화
gustn99 Jan 10, 2026
7a2ea2e
docs: 보너스 번호 조건 관련 오타 수정
gustn99 Jan 10, 2026
1ce3dc8
refactor: 전체 파일을 lotto 경로로 이동
gustn99 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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,58 @@
# javascript-planetlotto-precourse

### 기능 목록

1. 구매 금액 입력
2. 구매 개수 및 로또 번호 출력
1. 로또 1개 가격: 500원
2. 로또 번호는 1-30 사이 중복되지 않는 5개의 숫자
3. 당첨 번호 입력
1. 당첨 번호는 1-30 사이 중복되지 않는 5개의 숫자
2. 쉼표로 구분
4. 보너스 번호 입력
1. 당첨 번호와 중복되지 않는 1-30 사이 숫자
5. 당첨 번호 비교
- 1등: 5개 번호 일치 / 100,000,000원
- 2등: 4개 번호 + 보너스 번호 일치 / 10,000,000원
- 3등: 4개 번호 일치 / 1,500,000원
- 4등: 3개 번호 일치 + 보너스 번호 일치 / 500,000원
- 5등: 2개 번호 일치 + 보너스 번호 일치 / 5,000원
6. 당첨 통계 출력

### 테스트 사항

1. 정상 기능 동작
2. 구매 금액에 문자 입력 시 예외

### 실행 결과 예시

```md
구입금액을 입력해 주세요.
1000

2개를 구매했습니다.
[8, 11, 13, 21, 22]
[1, 3, 6, 14, 22]

당첨 번호를 입력해 주세요.
1, 2, 3, 4, 5

보너스 번호 번호를 입력해 주세요.
6

당첨 통계
---
5개 일치 (100,000,000원) - 0개
4개 일치, 보너스 번호 일치 (10,000,000원) - 0개
4개 일치 (1,500,000원) - 0개
3개 일치, 보너스 번호 일치 (500,000원) - 0개
2개 일치, 보너스 번호 일치 (5,000원) - 1개
0개 일치 (0원) - 1개
```

### 도전 과제

리팩터링

- 연습했던 대로 말고, 익숙하지 않은 방식으로
- getter를 줄이고, 되도록 tell하도록
28 changes: 28 additions & 0 deletions __tests__/LottoNumberTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import LottoNumber from '../src/lotto/LottoNumber.js';
import { LOTTO_NUMBER } from '../src/constants/lotto.js';

describe('LottoNumber 클래스', () => {
beforeEach(() => {
jest.restoreAllMocks();
});

test('equals: 두 LottoNumber를 비교하여 동일한 number를 가지면 true를 반환하다.', () => {
const lottoNumber1 = new LottoNumber(1);
const lottoNumber2 = new LottoNumber(1);
expect(lottoNumber1.equals(lottoNumber2)).toBe(true);
});

test('equals: 두 LottoNumber를 비교하여 서로 다른 number를 가지면 false를 반환하다.', () => {
const lottoNumber1 = new LottoNumber(1);
const lottoNumber2 = new LottoNumber(2);
expect(lottoNumber1.equals(lottoNumber2)).toBe(false);
});

test(`exception: 로또 번호가 ${LOTTO_NUMBER.MIN}보다 작으면 예외 발생한다.`, () => {
expect(() => new LottoNumber(LOTTO_NUMBER.MIN - 1)).toThrow();
});

test(`exception: 로또 번호가 ${LOTTO_NUMBER.MAX}보다 크면 예외 발생한다.`, () => {
expect(() => new LottoNumber(LOTTO_NUMBER.MAX + 1)).toThrow();
});
});
38 changes: 38 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Lotto from '../src/lotto/Lotto.js';
import LottoNumber from '../src/lotto/LottoNumber.js';

describe('Lotto 클래스', () => {
beforeEach(() => {
jest.restoreAllMocks();
});

test('includes: Lotto에 포함된 LottoNumber가 들어오면 true를 반환하다.', () => {
const lotto = new Lotto([1, 2, 3, 4, 5]);
const lottoNumber = new LottoNumber(1);
expect(lotto.includes(lottoNumber)).toBe(true);
});

test('includes: Lotto에 포함되지 않은 LottoNumber가 들어오면 false를 반환하다.', () => {
const lotto = new Lotto([1, 2, 3, 4, 5]);
const lottoNumber = new LottoNumber(6);
expect(lotto.includes(lottoNumber)).toBe(false);
});

test('matchCount: 두 Lotto를 비교하여 겹치는 숫자의 개수를 반환한다.', () => {
const lotto1 = new Lotto([1, 2, 3, 4, 5]);
const lotto2 = new Lotto([1, 2, 3, 4, 6]);
expect(lotto1.matchCount(lotto2)).toBe(4);
});

test('exception: 중복 번호가 포함되어 있으면 예외를 발생한다.', () => {
expect(() => new Lotto([1, 2, 3, 4, 4])).toThrow();
});

test('exception: 로또 번호 개수가 5보다 작으면 예외를 발생한다.', () => {
expect(() => new Lotto([1, 2, 3, 4])).toThrow();
});

test('exception: 로또 번호 개수가 5보다 크면 예외를 발생한다.', () => {
expect(() => new Lotto([1, 2, 3, 4, 5, 6])).toThrow();
});
});
70 changes: 69 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,73 @@
import { InputView, OutputView } from './view.js';
import Lotto from './lotto/Lotto.js';
import LottoNumber from './lotto/LottoNumber.js';
import LottoResult from './lotto/LottoResult.js';
import WinningLottoAndBonusNumberBuilder from './lotto/WinningLottoAndBonusNumberBuilder.js';
import LottoStore from './lotto/LottoStore.js';

class App {
async run() {}
constructor() {
this.inputView = InputView;
this.outputView = OutputView;
this.lottoStore = new LottoStore();
}

async run() {
const lottos = await this.createLotto();
this.outputView.printPurchasedLottos(lottos);

const winningLottoAndBonusNumber = await this.createWinningLottoAndBonusNumber();
const lottoResult = new LottoResult(lottos, winningLottoAndBonusNumber);
this.outputView.printResult(lottoResult);
}

async createLotto() {
return await this.#retryOnError(async () => {
const amount = await this.readAmount();
return this.lottoStore.purchase(amount);
});
}

async readAmount() {
return await this.#retryOnError(async () => {
return await this.inputView.askAmount();
});
}

async createWinningLottoAndBonusNumber() {
const builder = new WinningLottoAndBonusNumberBuilder();

await this.readWinningLotto(builder);
await this.readBonusNumber(builder);

return builder.build();
}

async readWinningLotto(builder) {
return await this.#retryOnError(async () => {
const winningLottoInput = await this.inputView.askWinningLotto();
const winningLotto = new Lotto(winningLottoInput);
builder.winningLotto(winningLotto);
});
}

async readBonusNumber(builder) {
return await this.#retryOnError(async () => {
const bonusNumberInput = await this.inputView.askBonusNumber();
const bonusNumber = new LottoNumber(bonusNumberInput);
builder.bonusNumber(bonusNumber);
});
}

async #retryOnError(callback) {
while (true) {
try {
return await callback();
} catch (error) {
this.outputView.printErrorMessage(error.message);
}
}
}
}

export default App;
17 changes: 17 additions & 0 deletions src/constants/lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const RANK = {
FIRST: 1,
SECOND: 2,
THIRD: 3,
FOURTH: 4,
FIFTH: 5,
ZERO: 0,
};

export const LOTTO_PRICE = 500;

export const LOTTO_NUMBER = {
MIN: 1,
MAX: 31,
};

export const LOTTO_LENGTH = 5;
42 changes: 42 additions & 0 deletions src/lotto/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import LottoNumber from './LottoNumber.js';
import { LOTTO_LENGTH } from '../constants/lotto.js';

class Lotto {
#lottoNumbers;

constructor(numbers) {
this.#validateNumbers(numbers);
this.#lottoNumbers = numbers.sort((a, b) => a - b).map((num) => new LottoNumber(num));
}

join(delimiter) {
const numbers = this.#lottoNumbers.map((lottoNumber) => lottoNumber.getNumber());
return numbers.join(delimiter);
}

includes(otherLottoNumber) {
return this.#lottoNumbers.some((lottoNumber) => lottoNumber.equals(otherLottoNumber));
}

matchCount(lotto) {
return this.#lottoNumbers.reduce((total, lottoNumber) => {
if (lotto.includes(lottoNumber)) {
return total + 1;
}
return total;
}, 0);
}

#validateNumbers(numbers) {
const numberSet = new Set(numbers);
if (numbers.length !== numberSet.size) {
throw new Error('중복 번호가 포함되어 있습니다.');
}

if (numbers.length !== LOTTO_LENGTH) {
throw new Error('로또 번호는 5개여야 합니다.');
}
}
}

export default Lotto;
26 changes: 26 additions & 0 deletions src/lotto/LottoNumber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LOTTO_NUMBER } from '../constants/lotto.js';

class LottoNumber {
#number;

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

#validate(number) {
if (number < LOTTO_NUMBER.MIN || number > LOTTO_NUMBER.MAX) {
throw new Error(`로또 번호는 ${LOTTO_NUMBER.MIN}부터 ${LOTTO_NUMBER.MAX} 사이의 숫자여야 합니다.`);
}
}

equals(lottoNumber) {
return this.#number === lottoNumber.getNumber();
}

getNumber() {
return this.#number;
}
}

export default LottoNumber;
40 changes: 40 additions & 0 deletions src/lotto/LottoResult.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { RANK } from '../constants/lotto.js';

class LottoResult {
#ranks;

constructor(lottos, winningLottoAndBonusNumber) {
this.#ranks = this.#calculateRanks(lottos, winningLottoAndBonusNumber);
}

#calculateRanks(lottos, winningLottoAndBonusNumber) {
const ranks = new Map(Object.values(RANK).map((rank) => [rank, 0]));

lottos.forEach((lotto) => {
const matchCount = winningLottoAndBonusNumber.matchCount(lotto);
const hasBonusNumber = winningLottoAndBonusNumber.hasBonusNumberIn(lotto);

const rank = this.#getRank(matchCount, hasBonusNumber);
if (Number.isInteger(rank)) {
ranks.set(rank, ranks.get(rank) + 1);
}
});

return ranks;
}

#getRank(matchCount, hasBonusNumber) {
if (matchCount === 5) return RANK.FIRST;
if (matchCount === 4 && hasBonusNumber) return RANK.SECOND;
if (matchCount === 4) return RANK.THIRD;
if (matchCount === 3 && hasBonusNumber) return RANK.FOURTH;
if (matchCount === 2 && hasBonusNumber) return RANK.FIFTH;
if (matchCount === 0 && !hasBonusNumber) return RANK.ZERO;
}

get(rank) {
return this.#ranks.get(rank);
}
}

export default LottoResult;
27 changes: 27 additions & 0 deletions src/lotto/LottoStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Random } from '@woowacourse/mission-utils';
import { LOTTO_LENGTH, LOTTO_NUMBER, LOTTO_PRICE } from '../constants/lotto.js';
import Lotto from './Lotto.js';

class LottoStore {
purchase(amount) {
this.#validateAmount(amount);
const count = Math.floor(amount / LOTTO_PRICE);

return Array.from({ length: count }, () => {
const randomNumbers = this.#getRandomNumbers();
return new Lotto(randomNumbers);
});
}

#validateAmount(amount) {
if (amount < LOTTO_PRICE) {
throw new Error(`로또는 1장에 ${LOTTO_PRICE}원입니다. ${LOTTO_PRICE}원 이상 입력해 주세요.`);
}
}

#getRandomNumbers() {
return Random.pickUniqueNumbersInRange(LOTTO_NUMBER.MIN, LOTTO_NUMBER.MAX, LOTTO_LENGTH);
}
}

export default LottoStore;
20 changes: 20 additions & 0 deletions src/lotto/WinningLottoAndBonusNumber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class WinningLottoAndBonusNumber {
#winningLotto;
#bonusNumber;

// builder를 통한 생성으로 제한
constructor(builder) {
this.#winningLotto = builder.getWinningLotto();
this.#bonusNumber = builder.getBonusNumber();
}

matchCount(lotto) {
return this.#winningLotto.matchCount(lotto);
}

hasBonusNumberIn(lotto) {
return lotto.includes(this.#bonusNumber);
}
}

export default WinningLottoAndBonusNumber;
Loading