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

## Functional requirements

### 간단한 로또 발매기를 구현한다.

- 로또 번호의 숫자 범위는 1~30까지이다.
- 1개의 로또를 발행할 때 중복되지 않는56개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 5개와 보너스 번호 1개를 뽑는다.
- 당첨은 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원
- 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
- 로또 1장의 가격은 500원이다.
- 당첨 번호와 보너스 번호를 입력받는다.
- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역을 출력하고 로또 게임을 종료한다.
- 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 해당 지점부터 다시 입력을 받는다.

### 프로그래밍 요구 사항

- 프로그램 종료 시 process.exit()를 호출하지 않는다.
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.
- 자바스크립트 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 JavaScript Style Guide를 원칙으로 한다.
- 기본으로 제공되는 테스트가 통과해야 한다.

### 도전 과제
기본 요구 사항을 모두 충족한 후, 아래 중 하나를 선택하여 도전하세요. 단, 도전 과제 수행 여부와 관계없이 기본 기능은 반드시 작동해야 합니다.

도전 방향
- 리팩터링: 작동은 그대로 유지하면서 코드 품질을 높이는 방향
- 기능 확장: 기본 기능 위에 새로운 기능을 추가하는 방향
💡 어떤 도전을 선택할지 고민된다면 프리코스 1~3주 차와 오픈 미션을 돌아보세요.

아쉬웠던 점은 무엇인가요?
- 다음에는 다르게 해보고 싶었던 것은 무엇인가요?
- 피드백을 받았지만 적용하지 못한 것은 무엇인가요?
정해진 정답은 없습니다. 본인의 프리코스 경험을 바탕으로 의미 있는 도전을 설계하세요.

---

[문제 설계](docs/DESIGN.md)
[TDD](docs/TDD_SOLVE.md)
[트러블 슈팅](docs/TROBLE_SHOOTING.md)
[트러블 슈팅](docs/Feature.md)

---
40 changes: 37 additions & 3 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe("로또 테스트", () => {
"[8, 11, 13, 21, 22]",
"[1, 3, 6, 14, 22]",
"당첨 통계",
"---",
"5개 일치 (100,000,000원) - 0개",
"4개 일치, 보너스 번호 일치 (10,000,000원) - 0개",
"4개 일치 (1,500,000원) - 0개",
Expand All @@ -80,7 +81,40 @@ describe("로또 테스트", () => {
});
});

test("예외 테스트", async () => {
await runException("500j");
});
// describe("예외 테스트", () => {
// beforeEach(() => {
// jest.restoreAllMocks();
// });

// mockQuestions(["1000", "1,2,3,4,5", "6"]);

// test.each([
// {
// name: "로또 번호의 숫자 범위는 1~30까지이다.",
// inputs: ["1000", "1,2,3,4,31", "6"],
// },
// {
// name: "로또 번호에 중복된 숫자가 있으면 안된다.",
// inputs: ["1000", "1,2,3,4,4", "6"],
// },
// {
// name: "구입 금액은 숫자로 입력해야 한다.",
// inputs: ["price", "1,2,3,4,5", "6"],
// },
// {
// name: "로또 번호의 개수는 5개여야 한다.",
// inputs: ["1000", "1,2,3,4,4", "6"],
// },
// ])("$name", async ({ inputs }) => {
// // given
// mockQuestions(inputs);

// // when
// const app = new App();

// // then
// await expect(app.run()).rejects.toThrow("[ERROR]");
// });
// });

});
33 changes: 33 additions & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 문제 설계

## 청사진

### 입력
1. 손님이 로또 구입 금액을 알려준다.
2. 손님이 본인의 당첨 번호를 알려준다.
3. 손님이 본인의 보너스 번호를 알려준다.


### 과정
1. 관리자는 구입 금액에 해당하는 만큼의 로또를 발행한다.
2. 관리자는 당첨 번호와 보너스 번호를 발행한 로또와 대조한다.


### 출력
1. 입력받은 입력 값을 확인해준다.
2. 입력받은 입력 값의 당첨 내역을 보여준다.
3. 손님을 돌려보낸다.

---


## 상세 설계

### 입력
1. 손님은 본인의 로또 번호를 명확하게 말해야한다.
- 손님이 이상하게 말하면 관리자는 다시 줄서기를 요구한다.
- 이는 에러 처리를 통해 프로그램을 종료하는 것으로 구현한다.
2. 관리자는 정확하게 로또를 발행하고 통계를 내야한다.
- 숫자 범위 1~30
- 발행시 추첨 시 중복되지 않는 5개의 숫자를 뽑기 + 보너스 번호 1개 뽑기
- 손님이 지불한 금액을 명확하게 확인하기 (장당 500원)
23 changes: 23 additions & 0 deletions docs/Feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 구현한 기능
기본적으로 제안서에서 제안된 기능을 모두 구현하였습니다.
1. 구입금액을 입력받는 기능
1. 구입한 금액을 토대로 로또를 발행
1. 발행한 로또와 손님의 로또번호를 비교
1. 통계 제시

# 도전

1. 객체를 최대한 분할하고, 상수값을 따로 관리하는 등의 유지보수가 최대한 가능하고 유연한 구조로 리펙토링 하려고 했습니다.

2. 추가기능을 구현했습니다. 아래는 해당하는 목록입니다.


# 추가기능

### 그 자리에서 직접 계산해서 손님에게 당첨 금액을 보여준다.
저의 입장에서 생각했을 떄, 수익률 같은 것도 있겠으나 손님 입장에서는 얼마를 벌었는지를 알려주는게 더 명확하다고 느껴졌습니다. 따라서, 마지막에 추가적으로 얼마를 벌었는지 관리자가 제시해줍니다.
이때 당첨이 안되면 부정적인 어구가 아니라 다음 기회에 도전할 수 있도록 얘기해서 다시 올수 있도록 합니다.

### 단위별로 돈을 받는 것은 이상하다.
손님은 700원을 줄 수도 있는데, 이에 대한 처리가 안되어 있습니다.
이에 Math.floor를 이용해서 돈을 거슬러 주도록 설계했습니다.
102 changes: 102 additions & 0 deletions docs/TDD_SOLVE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Test Driven Development
## 테스트로 주도한 계발의 과정을 서술합니다.

### 가장 중요한 것은 통계 결과이다.
해피케이스는 기본적으로 입력값을 신뢰한 결과이다.

1. 주어진 기능의 해피케이스 확인

mockRandoms([
[8, 11, 13, 21, 22],
[1, 3, 6, 14, 22],
]);
mockQuestions(["1000", "1,2,3,4,5", "6"]);

2. 이 해피 케이스에 대한 최소의 해결책 제시

- 손님의 input을 고정한다.
- 발행되는 로또를 상수로 고정한다.
- 통계 결과를 고정된 상수 값을 통해서 제시한다.

*결과*
당첨 통계
'---'
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개

---

3. 이제 실제 제시된 테스트 케이스와 동일하게 log가 나오도록 유도한다.
- 이때 output은 기존에 만들어져있는 view 클래스를 사용한다.
- 편의를 위해서 생략돼있던 '---' 를 통계 결과에 추가했다.
- 최소한의 결과가 해결되었다.

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

당첨 통계
'---'
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개

---

### 테스트 케이스가 해결되었으므로 더 나은 구조로 개선을 시작한다.
4. 이제 Input을 입력받을 수 있는 구조로 개선한다.
- Input과 Output 모두 제시되어 있는 view 객체를 사용한다.
- 여기서 Input 값은 제약이 많으므로 경계값에 대해서 유의하면서 테스트 코드를 작성한다.

*결과*
구입금액을 입력해 주세요.
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개

이제 예외 케이스에 대한 테스트 코드를 작성한다.
1. 구입 금액은 숫자로 입력해야 한다.
2. 로또 번호는 5개여야 한다.
3. 로또 번호는 1부터 30 사이의 숫자여야 한다.
4. 로또 번호에 중복된 숫자가 있으면 안된다.

---

5. 이제 로또 발행을 진행한다.

---

6. Jest를 사용한 예외 케이스 테스트가 없어졌다. 따라서 콘솔창에서 수동으로 예외케이스를 확인하려고 한다.
또한 main에 몰려있던 app에 몰려있었던 코드들을 객체로 단위별로 분리하는 리펙토링을 진행한다.
Output도 직접 만들었던 Output이 아니라 빌트인 Output을 이용해서 재 구현한다.

---

7. 검증 케이스를 추가한다.
기존에 있던 OutputView 객체에는 부족한 검증 케이스가 있다. 이를 추가 적용한다.
- 입력한 손님의 로또 번호 개수가 5개인지
- 입력한 손님의 로또 번호가 중복은 없는지

---
23 changes: 23 additions & 0 deletions docs/TROBLE_SHOOTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## 트러블 슈팅

### Rank 계산 간 오류

**문제**
여기서 일치하는 사람이 없는 경우에 대해서 기록되지 않았다.

this.lottos.forEach((lotto) => {
const rank = this.getRank(lotto);
if (rank) ranks[rank] += 1;
});

**해결**
요구사항에서 일치하지 않는 사람의 경우 인덱스는 0이다.
이때 rank가 0이면 if 문에서는 false를 반환하므로 넘어가버린다.
따라서 rank == 0 인 경우에도 true를 반환하도록 수정했다.

this.lottos.forEach((lotto) => {
const rank = this.getRank(lotto);
ranks[rank] += 1;
});

또한 Prize case에서도 FAIL: { count: 0, bonus: false, prize: 0 }, 을 추가하여 선언적인 검증 과정을 추가했다.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 84 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,88 @@
import { Console, Random } from '@woowacourse/mission-utils';
import { InputView, OutputView } from './view.js'
import Lotto from './Lotto.js';
import { SETTING, ERROR_MESSAGE,PRIZE } from './constants.js'
import { calculateTotalAmount } from './utils.js'

class App {
async run() {}
lottos = [];
winningNumbers = [];
bonusNumber = 0;

async run() {
try {
const amount = await InputView.askAmount();
this.publishLottos(amount);

let showLotto = [];
this.lottos.forEach(lotto => {
showLotto.push(lotto.getNumbers());
});
OutputView.printPurchasedLottos(showLotto);

// 손님의 로또 번호 입력
this.winningNumbers = await InputView.askWinningLotto();
if (this.winningNumbers.length !== SETTING.NUM_COUNT) {
throw new Error(`${ERROR_MESSAGE.INVALID_LENGTH}`);
}
const uniqueNumbers = new Set(this.winningNumbers);
if (uniqueNumbers.size !== this.winningNumbers.length) {
throw new Error(`${ERROR_MESSAGE.DUPLICATE_NUMBER}`);
}

this.bonusNumber = await InputView.askBonusNumber();

// Output 출력
const rankMap = new Map();
const ranks = this.calculateRanks();
for (let i =0; i<6 ; i++){
rankMap.set(i, ranks[i]);
}
OutputView.printResult(rankMap);

const totalAmount = calculateTotalAmount(ranks);
if(totalAmount == 0) Console.print("괜찮아요. 다음 기회를 노려봅시다!");
else Console.print("축하드립니다. " + totalAmount + "원에 당첨돼셨습니다!");

} catch (error) {
OutputView.printErrorMessage(error.message);
}
}

publishLottos(money) {
const count = money / SETTING.UNIT_PRICE;

for (let i = 0; i < count; i++) {
const numbers = Random.pickUniqueNumbersInRange(
SETTING.MIN_NUM,
SETTING.MAX_NUM,
SETTING.NUM_COUNT
);
this.lottos.push(new Lotto(numbers));
}
}

calculateRanks() {
const ranks = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
this.lottos.forEach((lotto) => {
const rank = this.getRank(lotto);
ranks[rank] += 1;
});
return ranks;
}

getRank(lotto) {
const matchCount = lotto.countMatch(this.winningNumbers);
const hasBonus = lotto.hasBonus(this.bonusNumber);

if (matchCount === PRIZE.FIRST.count) return 1;
if (matchCount === PRIZE.SECOND.count && hasBonus) return 2;
if (matchCount === PRIZE.THIRD.count) return 3;
if (matchCount === PRIZE.FOURTH.count && hasBonus) return 4;
if (matchCount === PRIZE.FIFTH.count && hasBonus) return 5;
if (matchCount === PRIZE.FAIL.count) return 0;
return 1000000;
}
}

export default App;
Loading