Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8955d7e
docs(README): 요구사항 분석 및 구현 사항 정리
geongyu09 Jan 10, 2026
ae76123
test(ValidatorTest): 사용자의 금액 입력을 검증하는 테스트 코드 작성
geongyu09 Jan 10, 2026
a7ae549
feat(Validator): 구입 금액은 500원 단위의 정수만 가능하도록 checkIsValidAmount 구현
geongyu09 Jan 10, 2026
88a6df3
fix(Validator): 에러 메시지에서 [ERROR] 접두사 제거
geongyu09 Jan 10, 2026
6b88130
feat(utils): 입력 값이 올바르지 않은 경우 에러 메시지를 출력하고 다시 입력을 받는 유틸 함수 구현
geongyu09 Jan 10, 2026
bffdfa7
feat(constants): "${개수}개를 구매했습니다." 라는 문구와 함께 구입한 로또의 개수와 번호를 출력
geongyu09 Jan 10, 2026
67d9f80
test(LottoTest): 로또 클래스에 대한 테스트 작성
geongyu09 Jan 10, 2026
3ee556c
feat(Lotto): 테스트에 맞추어 Lotto 클래스 구현
geongyu09 Jan 10, 2026
4539f5d
feat(utils): 랜덤한 로또 번호를 반환하는 유틸 함수 작성
geongyu09 Jan 10, 2026
3122a8f
fix(Lotto): 로또 번호를 6개로 입력 받을 수 있던 버그 수정
geongyu09 Jan 10, 2026
2918ee0
test(LottoDeviceTest): 입력받은 가격을 통해 구매 가능한 로또 개수를 반환하는 로직 작성
geongyu09 Jan 10, 2026
9df755f
feat(LottoDevice): 구메 내역 출력 로직 작성
geongyu09 Jan 10, 2026
697086e
test(ValidatorTest): 입력받은 당첨 번호를 검증하는 테스트 작성
geongyu09 Jan 10, 2026
d54bfa8
feat(Validator): 당첨 번호를 검증하는 로직 작성
geongyu09 Jan 10, 2026
a106341
feat(App): 당첨 번호를 입력받는 로직 작성
geongyu09 Jan 10, 2026
22d576f
test(ValidatorTest): 입력받은 보너스 점수가 올바른지 검증하는 테스트 작성
geongyu09 Jan 10, 2026
04d067c
feat(Validator): 보너스 번호 입력을 요구사항에 맞게 검증하는 로직 작성
geongyu09 Jan 10, 2026
3f0b2d6
feat(App): 보너스 번호를 입력받는 로직 작성
geongyu09 Jan 10, 2026
e7f254f
test(LottoDeviceTest): 입력받은 로또 객체와 번호, 보너스 번호를 통하여 랭크를 반환하는 테스트 작성
geongyu09 Jan 10, 2026
07ec9c5
feat(LottoDevice): 입력받은 로또 객체와 번호, 보너스 번호를 통하여 랭크를 반환하는 로직 작성
geongyu09 Jan 10, 2026
53ee274
feat(LottoDevice): 구매한 로또 당첨 결과를 반환하는 로직 작성
geongyu09 Jan 10, 2026
e4a5928
fix(App): 보너스 번호를 올바르게 검증하지 못하던 버그 수정
geongyu09 Jan 10, 2026
171008e
fix(LottoDevice): 아무것도 맞추지 못한 경우 카운트가 안되던 버그 수정
geongyu09 Jan 10, 2026
dbfa981
docs(README): 구현사항 문서 업데이트
geongyu09 Jan 10, 2026
1ac231b
chore: 사용하지 않는 코드 제거
geongyu09 Jan 10, 2026
faa57a3
refactor(Validator): 에러 메시지를 따로 상수화 하여 관리하도록 리팩토링
geongyu09 Jan 10, 2026
e88c893
refactor: 모든 함수가 15라인을 넘지 않도록 수정
geongyu09 Jan 10, 2026
1e3cc3b
refactor(Validator): 반복되는 검증 코드 추상화
geongyu09 Jan 10, 2026
d54b2dd
refactor(Validator): 당첨 번호 개수 상수화
geongyu09 Jan 10, 2026
ee18773
refactor: 추가적인 에러 메시지 추상화
geongyu09 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
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,108 @@
# javascript-planetlotto-precourse

## 요구사항 분석

### 1. 구입 금액 입력

- "구입금액을 입력해 주세요." 라는 문구와 함께 입력받는다.

#### 입력 요구사항

- 구입 금액은 500원 단위의 정수만 가능하다.
- 올바르지 않은 입력을 한 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 다시 입력 받는다.

### 2. 구매 내역 출력

- "${개수}개를 구매했습니다." 라는 문구와 함께 구입한 로또의 개수와 번호를 출력한다.
- 로또의 가격은 1개당 500원이며, 구매한 로또 개수는 구입금액 / 500 이다.
- 로또 번호는 1~30까지의 중복되지 않은 정수이다.
- 로또 번호는 오름차순으로 정렬해 보여준다.

### 3. 당첨 번호 입력

- "당첨 번호를 입력해 주세요."라는 문구와 함께 당첨 번호를 입력받는다.

#### 입력 요구사항

- 번호는 쉽표를 기준으로 구분한다.
- 번호는 1~30까지의 중복되지 않은 정수이다.
- 올바르지 않은 입력을 한 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 다시 입력 받는다.

e.g.

```
[ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다.
```

### 4. 보너스 번호 입력

- "보너스 번호 번호를 입력해 주세요." 문구와 함께 보너스 번호를 입력받는다.

#### 입력 요구사항

- 번호는 1~30까지의 중복되지 않은 정수이다.
- 이전에 입력한 당첨 번호와 중복될 수 없다.
- 올바르지 않은 입력을 한 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 다시 입력 받는다.

### 5. 당첨 통계 출력

- "당첨 통계\n---\n" 문구와 함께 당첨 통계를 출력한다.
- 당첨은 1등부터 5등까지 있으며, 당첨 기준과 금액은 아래와 같다.
- 1등: 5개 번호 일치 / 100,000,000원
- 2등: 4개 번호 + 보너스 번호 일치 / 10,000,000원
- 3등: 4개 번호 일치 / 1,500,000원
- 4등: 3개 번호 일치 + 보너스 번호 일치 / 500,000원
- 5등: 2개 번호 일치 + 보너스 번호 일치 / 5,000원

---

## 구현 사항

- [x] 1. 구입 금액 입력
- [x] "구입금액을 입력해 주세요." 라는 문구와 함께 입력 받음
- [x] 구입 금액은 500원 단위의 정수만 가능
- [x] 올바르지 않은 입력을 한 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 다시 입력 받음

- [x] 2. 구매 내역 출력
- [x] "${개수}개를 구매했습니다." 라는 문구와 함께 구입한 로또의 개수와 번호를 출력
- [x] 로또의 가격은 1개당 500원이며, 구매한 로또 개수는 `구입금액 / 500`
- [x] 로또 번호는 1~30까지의 중복되지 않은 정수
- [x] 로또 번호는 오름차순으로 정렬

- [x] 3. 당첨 번호 입력
- [x] "당첨 번호를 입력해 주세요."라는 문구와 함께 당첨 번호를 입력받는다.
- [x] 번호는 쉽표를 기준으로 구분
- [x] 번호는 1~30까지의 중복되지 않은 정수
- [x] 올바르지 않은 입력을 한 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 다시 입력 받음

- [x] 4. 보너스 번호 입력
- [x] "보너스 번호 번호를 입력해 주세요." 문구와 함께 보너스 번호를 입력받음
- [x] 번호는 1~30까지의 중복되지 않은 정수
- [x] 입력했던 당첨 번호와 중복되지 않음
- [x] 올바르지 않은 입력을 한 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 다시 입력 받음

- [x] 5. 당첨 통계 출력
- [x] "당첨 통계\n---\n" 문구와 함께 당첨 통계를 출력
- [x] 당첨 통계 게산
- [x]1등: 5개 번호 일치
- [x]2등: 4개 번호 + 보너스 번호 일치
- [x]3등: 4개 번호 일치
- [x]4등: 3개 번호 일치 + 보너스 번호 일치
- [x]5등: 2개 번호 일치 + 보너스 번호 일치

---

## 리팩토링 구현 사항

### 최소 조건

- [x] 모든 함수는 15줄을 넘지 않는다.
- [x] 모든 함수는 index level 2를 넘지 않는다. 단, class, try...cach 문의 들여쓰기는 제외한다.
- [x] 사용하지 않는 코드는 지운다.

### 추가 조건

- [x] (테스트 코드를 포함하여) 코드의 반복을 최대한 줄인다.
- [ ] 객체의 상태 접근과 관련한 리팩터링을 한다.
- [ ] 클래스는 단순히 값을 반환하는 메서드를 가지는 것 보다는 그 기능을 하도록 구현한다.
- [ ] 어떤 값을 감춰야 할지 고민한다.
26 changes: 26 additions & 0 deletions __tests__/LottoDeviceTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Lotto from "../src/Lotto.js";
import LottoDevice from "../src/LottoDevice.js";

describe("LottoDeviceTest", () => {
let lottoDevice;

beforeEach(() => {
lottoDevice = new LottoDevice();
});

describe("getCanIssueAmount", () => {
it("입력받은 가격을 통해 구매 가능한 로또 개수를 반환한다.", () => {
const amount = 1000;
const result = LottoDevice.getCanIssueAmount(amount);
expect(result).toBe(2);
});
});

describe("static calcRank", () => {
it("입력받은 로또 객체와 번호, 보너스 번호를 통하여 랭크를 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5]);
const result = LottoDevice.calcRank(lotto, [1, 2, 3, 4, 5], 7);
expect(result).toBe(1);
});
});
});
30 changes: 30 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Lotto from "../src/Lotto.js";

describe("LottoTest", () => {
it("로또의 가격은 500원이다.", () => {
const cost = Lotto.cost;
expect(cost).toBe(500);
});

describe("생성자에 수를 입력하여 로또를 발행한다.", () => {
it("6자리의 중복되지 않은 수를 입력해야한다.", () => {
const numbers = [1, 2, 3, 4, 5, 5];
expect(() => new Lotto(numbers)).toThrow();
});
it("수의 범위는 1~30이다. 범위를 넘으면 에러를 던진다.", () => {
expect(() => new Lotto([-1, 2, 3, 4, 5])).toThrow();
expect(() => new Lotto([1, 2, 3, 4, 31])).toThrow();
});
it("5자리 수를 입력받이 않으면 에러를 던진다.", () => {
const numbers = [1, 2, 3, 4, 5, 6];
expect(() => new Lotto(numbers)).toThrow();
});
});
describe("getLottoNumbers", () => {
it("발행한 로또의 번호를 가져온다.", () => {
const numbers = [1, 3, 4, 5, 6];
const lotto = new Lotto(numbers);
expect(lotto.getLottoNumbers()).toEqual([1, 3, 4, 5, 6]);
});
});
});
46 changes: 46 additions & 0 deletions __tests__/ValidatorTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Validator from "../src/Validator.js";

describe("ValidatorTest", () => {
describe("checkIsValidAmount", () => {
describe("올바른 입력을 받으면 true를 반환한다. ", () => {
it("구매 금액은 500원 단위의 양의 정수여야한다.", () => {
const testcase = 500;
const result = Validator.checkIsValidAmount(testcase);
expect(result).toBeTruthy();
});
});
describe("올바르지 않은 입력을 받으면 에러를 던진다.", () => {
it.each([123, -500])("%s 를 입력받으면 에러를 던진다.", (testcase) => {
expect(() => Validator.checkIsValidAmount(testcase)).toThrow();
});
});
});
describe("checkWinningNumber", () => {
describe("올바른 입력을 받으면 true를 반환한다. ", () => {
it("당첨 번호는 1~30의 양의 정수 5개이다.", () => {
const testcase = [1, 2, 3, 4, 5];
const result = Validator.checkWinningNumber(testcase);
expect(result).toBeTruthy();
});
});
// TODO: 추후 작성하기
// describe("올바르지 않은 입력을 받으면 에러를 던진다.", () => {
// it.each([123, -500])("%s 를 입력받으면 에러를 던진다.", (testcase) => {
// expect(() => Validator.checkWinningNumber(testcase)).toThrow();
// });
// });
});

describe("checkBonusNumber", () => {
describe("올바른 입력을 받으면 true를 반환한다.", () => {
it("예외 번호를 제외한 1 ~ 30의 양의 정수이다.", () => {
expect(Validator.checkBonusNumber(1, [7, 8, 9, 11])).toBeTruthy();
});
});
describe("올바르지 않은 입력을 받은 경우 에러를 던진다.", () => {
it("예외 번호에 포함된 번호 입력시 에러 발생", () => {
expect(() => Validator.checkBonusNumber(1, [1, 8, 9, 11])).toThrow();
});
});
});
});
59 changes: 58 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
import LottoDevice from "./LottoDevice.js";
import { handleInputError } from "./utils.js";
import Validator from "./Validator.js";
import { InputView, OutputView } from "./view.js";

class App {
async run() {}
async getAmount() {
const amount = await handleInputError({
input: InputView.askAmount,
validator: Validator.checkIsValidAmount,
});

return amount;
}

printPurchase(amount) {
const lottoDevice = new LottoDevice();

lottoDevice.issueLottos(amount);
const lottos = lottoDevice.getLottos();
OutputView.printPurchasedLottos(lottos);

return lottoDevice;
}

async getWinningNumber() {
const winningNumbers = await handleInputError({
input: InputView.askWinningLotto,
validator: Validator.checkWinningNumber,
});

return winningNumbers;
}

async getBonusNumber(winningNumbers) {
const bonusNumber = await handleInputError({
input: InputView.askBonusNumber,
validator: (input) => Validator.checkBonusNumber(input, [...winningNumbers]),
});

return bonusNumber;
}

printResult(lottoDevice, winningNumbers, bonusNumber) {
const lottoServiceResult = lottoDevice.getRankResult(winningNumbers, bonusNumber);
OutputView.printResult(lottoServiceResult);
}

async run() {
const amount = await this.getAmount();

const lottoDevice = this.printPurchase(amount);

const winningNumbers = await this.getWinningNumber();

const bonusNumber = await this.getBonusNumber(winningNumbers);

this.printResult(lottoDevice, winningNumbers, bonusNumber);
}
}

export default App;
30 changes: 30 additions & 0 deletions src/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import ERROR_MESSAGE from "./constants/errorMessages.js";

class Lotto {
#numbers;

static cost = 500;

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

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

const set = new Set();
numbers.forEach((n) => {
if (n > 30 || n < 1) throw new Error(ERROR_MESSAGE.LOTTO.LOTTO_RANGE);
set.add(n);
});
if (set.size !== numbers.length) throw new Error(ERROR_MESSAGE.LOTTO.LOTTO_DUPLICATE);
}

// TODO: 단순 가져오기보다는 일을 할 수 있도록 리팩토링 필요
getLottoNumbers() {
return this.#numbers;
}
}

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

const DEFAULT_RANKS = [
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
[0, 0],
];

class LottoDevice {
#lottos;

constructor() {
this.#lottos = [];
}

issueLottos(amount) {
const issuedCount = LottoDevice.getCanIssueAmount(amount);
while (this.#lottos.length !== issuedCount) {
const numbers = LottoDevice.getRandomLottoNumbers();
const lotto = new Lotto(numbers);
this.#lottos.push(lotto);
}
}

getLottos() {
return this.#lottos.map((lotto) => lotto.getLottoNumbers());
}

static getCanIssueAmount(amount) {
return amount / Lotto.cost;
}

static getRandomLottoNumbers() {
return Random.pickUniqueNumbersInRange(1, 30, 5).sort((a, b) => a - b);
}

getRankResult(winningNumbers, bonusNumber) {
const ranks = new Map(DEFAULT_RANKS);

this.#lottos.forEach((lotto) => {
const rank = LottoDevice.calcRank(lotto, winningNumbers, bonusNumber);
const prev = ranks.get(rank);
ranks.set(rank, prev + 1);
});

return ranks;
}

static calcRank(lotto, numbers, bonusNumber) {
let correctCount = 0;
const lottoNumbers = lotto.getLottoNumbers();

numbers.forEach((n) => {
if (lottoNumbers.includes(n)) correctCount += 1;
});
const isBonusNumCorrect = lottoNumbers.includes(bonusNumber);

if (correctCount === 5) return 1;
if (correctCount === 4 && isBonusNumCorrect) return 2;
if (correctCount === 4) return 3;
if (correctCount === 3 && isBonusNumCorrect) return 4;
if (correctCount === 2 && isBonusNumCorrect) return 5;
return 0;
}
}

export default LottoDevice;
Loading