Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7a3d1fe
docs: 구현할 기능 목록 정리
bel1c10ud Jan 10, 2026
d5e7346
feat: 잘못된 값이 입력된 경우 해당 지점부터 다시 입력 받는 기능 구현
bel1c10ud Jan 10, 2026
176b52d
feat: 당첨 번호와 보너스 번호에 대한 검증 도입
bel1c10ud Jan 10, 2026
aa064c9
docs: 당첨 번호와 보너스 번호에 대한 검증 조건 추가
bel1c10ud Jan 10, 2026
46e57ce
feat: 당첨 번호와 보너스 번호에 대해 갯수와 양수 여부 검증 추가
bel1c10ud Jan 10, 2026
8707388
fix: 검증 함수 호출시 잘못 지정된 인자 수정
bel1c10ud Jan 10, 2026
424d851
feat: 구입 금액에 해당하는 로또 구입 및 구입한 로또 출력
bel1c10ud Jan 10, 2026
e1d80ec
feat: 당첨 통계 기능 구현
bel1c10ud Jan 10, 2026
6ea0e69
docs: 도전 과제 업데이트
bel1c10ud Jan 10, 2026
0e16db3
refactor: 매직 넘버 제거
bel1c10ud Jan 10, 2026
9e19842
refactor: 검증 함수 별도 파일로 분리 및 App.run() 함수 나누기
bel1c10ud Jan 10, 2026
3462b09
refactor: printEmptyLine 위치 조정
bel1c10ud Jan 10, 2026
06fa085
refactor: Lotto, WinningLotto 객체 도입
bel1c10ud Jan 10, 2026
79a4de5
refactor: publishTickets 함수 LottoStore 안으로 이동
bel1c10ud Jan 10, 2026
5089105
refactor: LottoStore 인스턴스를 메서드가 아닌 객체 상태에 두고 사용하도록 변경
bel1c10ud Jan 10, 2026
dc2c124
refactor: WinningLotto.evaluateTicket() 메서드 비공개 메서드로 전환
bel1c10ud Jan 10, 2026
b38a840
chore: Validator 유틸리티 클래스 utility 폴더로 이동
bel1c10ud Jan 10, 2026
79ec404
refactore: lottoController 도입
bel1c10ud Jan 10, 2026
317a023
docs: 구현한 기능 목록 업데이트
bel1c10ud Jan 10, 2026
c132b84
test: 테스트 코드 추가
bel1c10ud Jan 10, 2026
894e71b
chore: 누락된 async 함수 접미사 수정
bel1c10ud Jan 10, 2026
eb10873
refactor: 출력 형식 변경
bel1c10ud Jan 10, 2026
2c0ad8f
refactor: 매직넘버 제거
bel1c10ud 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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# javascript-planetlotto-precourse

## 구현할 기능 목록

- [x] 사용자 입력 처리하기
- [x] 로또 구입 금액 입력
- [x] 당첨 번호 입력
- [x] 보너스 번호 입력
- [x] 잘못된 값이 입력된 경우 해당 지점부터 다시 입력 받기
- [x] 잘못된 값에 대한 예외 처리
- [x] 로또 구입 금액으로 숫자가 아닌 값이 들어오는 경우
- [x] 당첨 번호로 숫자가 아닌 값이 들어오는 경우
- [x] 당첨 번호로 양수가 아닌 값이 들어오는 경우
- [x] 당첨 번호로 정수가 아닌 값이 들어오는 경우
- [x] 당첨 번호로 1부터 30이 아닌 값이 들어오는 경우
- [x] 당첨 번호가 5개가 아닌 경우
- [x] 당첨 번호가 서로 중복되는 경우
- [x] 보너스 번호로 숫자가 아닌 값이 들어오는 경우
- [x] 보너스 번호로 양수가 아닌 값이 들어오는 경우
- [x] 보너스 번호로 정수가 아닌 값이 들어오는 경우
- [x] 보너스 번호로 1부터 30이 아닌 값이 들어오는 경우
- [x] 보너스 번호가 당첨 번호와 중복되는 경우
- [x] 로또 구입 금액만큼 로또 발행
- [x] 구입한 로또 출력
- [x] 로또 번호는 오름차순 정렬
- [x] 당첨 통계 출력
- [ ] 코드 리팩토링
- [x] 매직 넘버 제거
- [x] mvc 패턴 도입
- [x] 테스트 코드 작성
17 changes: 17 additions & 0 deletions __tests__/LottoStoreTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import LottoStore from '../src/model/lottoStore.js';

describe('LottoStore 클래스 테스트', () => {
describe('publishTicketsByAmount() 메서드 테스트', () => {
test('구입 금액이 1000원인 경우 2장을 발급', () => {
const lottoStore = new LottoStore();
const lottoTickets = lottoStore.publishTicketsByAmount(1000);
expect(lottoTickets).toHaveLength(2);
});

test('구입 금액이 0인 경우 로또를 발행하지 않음', () => {
const lottoStore = new LottoStore();
const lottoTickets = lottoStore.publishTicketsByAmount(0);
expect(lottoTickets).toHaveLength(0);
});
});
});
8 changes: 8 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Lotto from '../src/model/lotto.js';

describe('Lotto 클래스 테스트', () => {
test('로또 번호가 오름차순으로 정렬되어 저장됨', () => {
const ticket = new Lotto([5, 4, 3, 2, 1]);
expect(ticket.getNumbers()).toEqual([1, 2, 3, 4, 5]);
});
});
34 changes: 34 additions & 0 deletions __tests__/ValidatorTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { LOTTO } from '../src/constants.js';
import Validator from '../src/utility/validator.js';

describe('Validator 테스트', () => {
test('배열안에 중복되는 값이 있는 경우 예외 처리', () => {
expect(() => {
Validator.validateUnique([1, 1, 2], '');
}).toThrow('');
});

test(`배열의 길이가 ${LOTTO.COUNT}개를 충족하지 않는 경우 예외 처리`, () => {
expect(() => {
Validator.validateCount([1, 2, 3, 4], '');
}).toThrow('');
});

test('입력값이 정수가 아닌 경우 예외 처리', () => {
expect(() => {
Validator.validateInteger(3.14, '');
}).toThrow('');
});

test('입력값이 양수가 아닌 경우 예외 처리', () => {
expect(() => {
Validator.validatePositive(-1, '');
}).toThrow('');
});

test(`입력값이 ${LOTTO.MIN_NUMBER}에서 ${LOTTO.MAX_NUMBER}까지의 값이 아닌 경우 예외 처리`, () => {
expect(() => {
Validator.validateRange(31, '');
}).toThrow('');
});
});
42 changes: 42 additions & 0 deletions __tests__/WinningLottoTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Lotto from '../src/model/lotto.js';
import WinningLotto from '../src/model/winningLotto.js';

describe('WinningLotto 클래스 테스트', () => {
describe('getNumbers() 메서드 테스트', () => {
test('당첨 번호를 반환', () => {
const winningLotto = new WinningLotto([1, 2, 3, 4, 5], 6);
expect(winningLotto.getNumbers()).toEqual([1, 2, 3, 4, 5]);
});
});

describe('getBonusNumber() 메서드 테스트', () => {
test('보너스 번호를 반환', () => {
const winningLotto = new WinningLotto([1, 2, 3, 4, 5], 6);
expect(winningLotto.getBonusNumber()).toBe(6);
});
});

describe('evaluateTickets() 메서드 테스트', () => {
test('로또 평가 결과를 반환', () => {
const winningLotto = new WinningLotto([1, 2, 3, 4, 5], 6);
const tickets = [
new Lotto([30, 29, 28, 27, 26]),
new Lotto([1, 2, 3, 4, 5]),
new Lotto([1, 2, 3, 4, 6]),
new Lotto([1, 2, 3, 4, 7]),
new Lotto([1, 2, 3, 6, 7]),
new Lotto([1, 2, 6, 7, 8]),
];
expect(winningLotto.evaluateTickets(tickets)).toEqual(
new Map([
[0, 1],
[1, 1],
[2, 1],
[3, 1],
[4, 1],
[5, 1],
]),
);
});
});
});
7 changes: 6 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import LottoConroller from './controller/lottoController.js';

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

export default App;
15 changes: 15 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const LOTTO = {
PRICE: 500,
COUNT: 5,
MIN_NUMBER: 1,
MAX_NUMBER: 30,
};

export const RANK = {
FIRST: 1,
SECOND: 2,
THIRD: 3,
FOURTH: 4,
FIFTH: 5,
NONE: 0,
};
36 changes: 36 additions & 0 deletions src/controller/lottoController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { InputView, OutputView } from './../view.js';
import WinningLotto from './../model/winningLotto.js';
import LottoStore from './../model/lottoStore.js';

class LottoConroller {
#lottoStore;

constructor() {
this.#lottoStore = new LottoStore();
}

async start() {
const tickets = await this.purchaseLottosAsync();
OutputView.printPurchasedLottos(tickets.map((ticket) => ticket.getNumbers()));

const { winningNumbers, bonusNumber } = await this.getLastWinningLottoAsync();
const winningLotto = new WinningLotto(winningNumbers, bonusNumber);

const counts = winningLotto.evaluateTickets(tickets);
OutputView.printResult(counts);
}

async purchaseLottosAsync() {
const amount = await InputView.getAmountAsync();
const tickets = this.#lottoStore.publishTicketsByAmount(amount);
return tickets;
}

async getLastWinningLottoAsync() {
const winningNumbers = await InputView.getWinningLottoAsync();
const bonusNumber = await InputView.getBonusNumberAsync(winningNumbers);
return { winningNumbers, bonusNumber };
}
}

export default LottoConroller;
13 changes: 13 additions & 0 deletions src/model/lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Lotto {
#numbers;

constructor(numbers) {
this.#numbers = [...numbers].sort((a, b) => a - b);
}

getNumbers() {
return [...this.#numbers];
}
}

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

class LottoStore {
#randomLottoNumbersGenerator;

constructor(randomLottoNumbersGenerator) {
this.#randomLottoNumbersGenerator =
randomLottoNumbersGenerator ??
(() => Random.pickUniqueNumbersInRange(LOTTO.MIN_NUMBER, LOTTO.MAX_NUMBER, LOTTO.COUNT));
}

publishTicketsByAmount(amount) {
const tickets = [];
const count = Math.floor(amount / LOTTO.PRICE);

for (let i = 1; i <= count; i++) {
tickets.push(new Lotto([...this.#randomLottoNumbersGenerator()]));
}

return tickets;
}
}

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

class WinningLotto extends Lotto {
#bonusNumber;

constructor(numbers, bonusNumber) {
super(numbers);
this.#bonusNumber = bonusNumber;
}

getBonusNumber() {
return this.#bonusNumber;
}

#evaluateTicket(numbers) {
const matchCount = numbers.filter((number) => this.getNumbers().includes(number)).length;
const hasBonus = numbers.includes(this.#bonusNumber);

if (matchCount === 5) return RANK.FIRST;
if (matchCount === 4 && hasBonus) return RANK.SECOND;
if (matchCount === 4) return RANK.THIRD;
if (matchCount === 3 && hasBonus) return RANK.FOURTH;
if (matchCount === 2 && hasBonus) return RANK.FIFTH;
return RANK.NONE;
}

evaluateTickets(tickets) {
const counts = [RANK.NONE, RANK.FIRST, RANK.SECOND, RANK.THIRD, RANK.FOURTH, RANK.FIFTH].reduce((acc, rank) => {
acc.set(rank, 0);
return acc;
}, new Map());

tickets.forEach((ticket) => {
const grade = this.#evaluateTicket(ticket.getNumbers());
counts.set(grade, counts.get(grade) + 1);
});

return counts;
}
}

export default WinningLotto;
56 changes: 56 additions & 0 deletions src/utility/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { LOTTO } from '../constants.js';

const Validator = {
validateUnique(numbers, message) {
if (new Set([...numbers]).size !== numbers.length) {
throw new Error(message);
}
},

validateCount(numbers, message) {
if (numbers.length !== LOTTO.COUNT) {
throw new Error(message);
}
},

validatePositive(number, message) {
if (number < 1) {
throw new Error(message);
}
},

validateInteger(number, message) {
if (number % 1 !== 0) {
throw new Error(message);
}
},

validateRange(number, message) {
if (number < LOTTO.MIN_NUMBER || number > LOTTO.MAX_NUMBER) {
throw new Error(message);
}
},

validateLottoNumbers(lottoNumbers) {
this.validateCount(lottoNumbers, '당첨 번호는 5개입니다.');
this.validateUnique(lottoNumbers, '당첨 번호는 서로 중복될 수 없습니다.');

lottoNumbers.forEach((number) => {
this.validatePositive(number, '당첨 번호는 양수이어야 합니다.');
this.validateInteger(number, '당첨 번호는 정수이어야 합니다.');
this.validateRange(number, `당첨 번호는 ${LOTTO.MIN_NUMBER}에서 ${LOTTO.MAX_NUMBER}까지의 정수이어야합니다.`);
});
},

validateBonusNumber(bonusNumber, lottoNumbers) {
this.validatePositive(bonusNumber, '보너스 번호는 양수이어야 합니다.');
this.validateInteger(bonusNumber, '보너스 번호는 정수이어야 합니다.');
this.validateRange(
bonusNumber,
`보너스 번호는 ${LOTTO.MIN_NUMBER}에서 ${LOTTO.MAX_NUMBER}까지의 정수이어야합니다.`,
);
this.validateUnique([bonusNumber, ...lottoNumbers], '보너스 번호는 당첨 번호와 중복될 수 없습니다.');
},
};

export default Validator;
Loading