Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[클린코드 5기 김경현] 로또 미션 STEP 2 #245

Merged
merged 8 commits into from
Sep 5, 2023
79 changes: 79 additions & 0 deletions docs/STEP2-REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## 🚀 2단계 - 재시작 기능 및 UX 개선

📍 학습 목표

- UI와 도메인 영역을 분리할 수 있는 설계를 고민해보고, 목적에 맞게 객체와 함수를 활용
- 단위 테스트 기반으로 점진적인 리팩터링

### 🎯 기능 요구 사항

- [x] 로또 번호는 오름차순으로 정렬하여 보여준다.
- [x] 당첨 통계를 출력한 뒤에는 재시작/종료 여부를 입력받는다.
- [x] 재시작할 경우 구입 금액 입력부터 게임을 다시 시작하고, 종료하는 경우 그대로 프로그램을 종료시킨다.
- [x] 사용자가 잘못된 값을 입력한 경우 throw문을 사용해 예외를 발생시키고, 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.

```
> 구입금액을 입력해 주세요.8000
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]

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

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

당첨 통계
--------------------
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.

> 다시 시작하시겠습니까? (y/n)
```

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

<u> 이전 미션의 프로그래밍 요구 사항은 기본으로 포함한다. </u>

**예측 가능하고, 실수를 방지할 수 있는 코드를 작성하기 위해 노력한다.**

- 변수 선언시 const 만 사용한다.
- 함수(또는 메서드)의 들여쓰기 depth는 1단계까지만 허용한다.
- 함수의 매개변수는 2개 이하여야 한다.
- 함수에서 부수 효과를 분리하고, 가능한 순수 함수를 많이 활용한다.

**테스트하기 쉬운 코드에 대해 고민하고, 문제를 작은 단위로 쪼개서 접근하는 방식을 연습한다.**

- 모든 기능을 TDD로 구현하는 것을 시도하여, 테스트 할 수 있는 도메인 로직에 대해서는 모두 단위 테스트가 존재해야 한다. (단, UI 로직은 제외)

**모듈화와 객체 간에 로직을 재사용하는 방법에 대해 고민한다.**

- 로또 번호와 당첨 로또 번호의 유효성 검사시 발생하는 중복 코드를 제거해야 한다.
- 클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다.
- getter를 금지하는 것이 아니라 말 그대로 프로퍼티 자체를 그대로 꺼내서 객체 바깥에서 직접 조작하는 등의 작업을 지양하자는 의미입니다 :) 객체 내부에서 알아서 할 수 있는 일은 객체가 스스로 할 수 있게 맡겨주세요.
- 클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

### 📝 과제 진행 요구사항

**기능 목록 및 commit 로그 요구 사항**

- 기능을 구현하기 전에 docs/REQUIREMENTS.md 파일에 구현할 기능 목록을 정리해 추가한다.
- git의 commit 단위는 앞 단계에서 REQUIREMENTS.md 파일에 정리한 기능 목록 단위로 추가한다.

**실행 환경 확인**

<u>1~2단계는 콘솔 기반, 2~3단계는 웹 기반으로 진행하게 된다.</u>

- 1단계는 npm run start-step1 커맨드로 앱을 실행할 수 있도록 한다.
- 앱의 진입점이 되는 파일은 src/step1-index.js
- 2단계는 npm run start-step2 커맨드로 앱을 실행할 수 있도록 한다.
- 앱의 진입점이 되는 파일은 src/step2-index.js
4 changes: 3 additions & 1 deletion docs/paintIdeas.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
- 도메인 분리
- 변수명 관리
- 코드의 가독성과 유지보수성을 높일 수 있습니다.
- 순수 함수 처리
- 순수 함수 처리 & test code
- scope
- 테스트 코드
- modules - export & import (jsdoc and js files)
- BDD vs TDD vs ATDD
51 changes: 51 additions & 0 deletions feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## feedback

- "return 구문으로 if-else의 스코프를 줄일 수 있을 것 같습니다!"
-> 조건에 맞으면, 다음 실행 못하게 return 해주는, 생각하신 의도가 맞는지 궁금합니다.

```js
if (restartOrExitLotto.toLowerCase() !== "y") {
return rl.close();
} else {
initializeDataStorage();
return startLottoGame();
}
```

- "메세지를 다르게 가져가거나, 조건문을 합쳐도 괜찮을 것 같네요!"
-> 조건문을 합치는 방향으로 정했습니다.

```js
export const validatePositiveNumber = num => {
if (!isValidNumber(num) || isNaN(num) || num <= 0) {
throw new Error(ERROR_MESSAGE[VALID_NUMBER_REQUIRED]);
}

// if (num <= 0) {
// throw Error(ERROR_MESSAGE[VALID_NUMBER_REQUIRED]);
// }
};
```

- "\_ 접두어는 어떤 의미일까요?"

> 다른 함수들과 헷갈리지 않게, 파일안에서만 고유하게 쓸 수 있는 함수 컨벤션을 만들어봤습니다.

- "앱 전반에 switch-case 구문이 많이 보이는데, 가독성을 헤치는 주 원인 중 하나로 꼽는 문법입니다. 만약 좀 더 좋게 바꾸려면 어떻게 해야 할까요?"
-> if/else 보다, 가독성이 좋을 것 같아 switch-case로 접근을 하였으나, 객체로 묶어서 관리하는편이 나을 것 같다고 다시 판단했습니다. 파일을 깔끔하게 쓰기 위해, jsdoc 파일을 분리하였습니다.

```js
/** @typedef {RankPriceFunctions} */
const RANK_PRICE_STATS = {
firstWinner: count =>
`6개 일치 (${LOTTO_PRICE.FIRST_WINNER.toLocaleString()}원) - ${count}개`,
secondWinner: count =>
`5개 일치, 보너스 볼 일치 (${LOTTO_PRICE.SECOND_WINNER.toLocaleString()}원) - ${count}개`,
thirdWinner: count =>
`5개 일치 (${LOTTO_PRICE.THIRD_WINNER.toLocaleString()}원) - ${count}개`,
fourthWinner: count =>
`4개 일치 (${LOTTO_PRICE.FOURTH_WINNER.toLocaleString()}원) - ${count}개`,
fifthWinner: count =>
`3개 일치 (${LOTTO_PRICE.FIFTH_WINNER.toLocaleString()}원) - ${count}개`,
};
```
21 changes: 21 additions & 0 deletions jsdoc_comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// https://github.com/microsoft/TypeScript/issues/50436

/**
* @typedef {Object} RankPriceStatsFunctions
* @property {(count: number) => string} firstWinner
* @property {(count: number) => string} secondWinner
* @property {(count: number) => string} thirdWinner
* @property {(count: number) => string} fourthWinner
* @property {(count: number) => string} fifthWinner
*/

/**
* @typedef {Object} LottoStats
* @property {number} firstWinner - 6
* @property {number} secondWinner - 5 + bonus
* @property {number} thirdWinner - 5
* @property {number} fourthWinner - 4
* @property {number} fifthWinner - 3
*/

module.exports = {};
12 changes: 11 additions & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"license": "MIT",
"scripts": {
"start-step1": "node src/step1-index.js",
"start-step2": "node src/step2-index.js",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"devDependencies": {
Expand Down
60 changes: 18 additions & 42 deletions src/UI/View/OutputView.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,35 @@
import { LOTTO_PRICE } from "../../domain/constant.js";

/**
* @param {number} profitRate
* @returns
*/
/** @type {import('../../../jsdoc_comment.js').RankPriceStatsFunctions} */
const RANK_PRICE_STATS = {
firstWinner: count =>
`6개 일치 (${LOTTO_PRICE.FIRST_WINNER.toLocaleString()}원) - ${count}개`,
secondWinner: count =>
`5개 일치, 보너스 볼 일치 (${LOTTO_PRICE.SECOND_WINNER.toLocaleString()}원) - ${count}개`,
thirdWinner: count =>
`5개 일치 (${LOTTO_PRICE.THIRD_WINNER.toLocaleString()}원) - ${count}개`,
fourthWinner: count =>
`4개 일치 (${LOTTO_PRICE.FOURTH_WINNER.toLocaleString()}원) - ${count}개`,
fifthWinner: count =>
`3개 일치 (${LOTTO_PRICE.FIFTH_WINNER.toLocaleString()}원) - ${count}개`,
};

/** @param {number} profitRate */
const ViewProfitRate = profitRate =>
console.log(`총 수익률: ${profitRate.toFixed(1)}%`);

/**
* @param {number[][]} buyerLottoList
*/
/** @param {number[][]} buyerLottoList */
const ViewBuyerLottoList = buyerLottoList => {
buyerLottoList.forEach(list => console.log(list));
};

/**
* @typedef {Object} LottoStats
* @property {number} firstWinner - 6
* @property {number} secondWinner - 5 + bonus
* @property {number} thirdWinner - 5
* @property {number} fourthWinner - 4
* @property {number} fifthWinner - 3
*/
/** @type {import('../../../jsdoc_comment.js').LottoStats} */
const ViewWinnerPriceStats = stats => {
console.log("당첨 통계");
console.log("--------------------");

for (const [rank, count] of Object.entries(stats)) {
switch (rank) {
case "firstWinner":
console.log(
`6개 일치 (${LOTTO_PRICE.FIRST_WINNER.toLocaleString()}원) - ${count}개`
);
break;
case "secondWinner":
console.log(
`5개 일치, 보너스 볼 일치 (${LOTTO_PRICE.SECOND_WINNER.toLocaleString()}원) - ${count}개`
);
break;
case "thirdWinner":
console.log(
`5개 일치 (${LOTTO_PRICE.THIRD_WINNER.toLocaleString()}원) - ${count}개`
);
break;
case "fourthWinner":
console.log(
`4개 일치 (${LOTTO_PRICE.FOURTH_WINNER.toLocaleString()}원) - ${count}개`
);
break;
case "fifthWinner":
console.log(
`3개 일치 (${LOTTO_PRICE.FIFTH_WINNER.toLocaleString()}원) - ${count}개`
);
break;
}
console.log(RANK_PRICE_STATS[rank](count));
}
};

Expand Down
28 changes: 12 additions & 16 deletions src/domain/LottoMainCompany.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,22 @@ const _handleLottoMatchNumberLength = (buyerLottoNumbers, winNumbers) =>
const _hasBonusNumber = buyerList =>
buyerList.includes(dataStorage.bonusNumber);

const _RANK_STATS = {
6: () => dataStorage.lottoStats.firstWinner++,
5: buyerList =>
!_hasBonusNumber(buyerList)
? dataStorage.lottoStats.thirdWinner++
: dataStorage.lottoStats.secondWinner++,
4: () => dataStorage.lottoStats.fourthWinner++,
3: () => dataStorage.lottoStats.fifthWinner++,
};

/**
* @param {number} match - 일치하는 숫자 개수
* @param {number[]} buyerList
*/
const _handleRankStats = (match, buyerList) => {
switch (match) {
case 3:
dataStorage.lottoStats.fifthWinner++;
break;
case 4:
dataStorage.lottoStats.fourthWinner++;
break;
case 5:
!_hasBonusNumber(buyerList)
? dataStorage.lottoStats.thirdWinner++
: dataStorage.lottoStats.secondWinner++;
break;
case 6:
dataStorage.lottoStats.firstWinner++;
break;
default:
if (_RANK_STATS.hasOwnProperty(match)) {
_RANK_STATS[match](buyerList);
}
};
1 change: 0 additions & 1 deletion src/domain/LottoStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { validatePositiveNumber } from "../utils/validator.js";
// LottoStore: caculate lotto amount
export const LottoStore = {
calculateLottoAmount(amount) {
validatePositiveNumber(amount);
validateLottoPrice(amount);

return Math.floor(amount / LOTTO_PRICE.PRICE);
Expand Down
4 changes: 0 additions & 4 deletions src/domain/Machine.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ export const Machine = {
validateLottoDuplicateNumbers(numberList);

dataStorage.winNumbers = numberList;

return dataStorage.winNumbers;
},

getBonusNumber(winNumbers, bonusNumberStr) {
Expand All @@ -35,7 +33,5 @@ export const Machine = {
validateBonusNumber(winNumbers, bonusNumber);

dataStorage.bonusNumber = bonusNumber;

return dataStorage.bonusNumber;
},
};
2 changes: 1 addition & 1 deletion src/domain/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const INFO_MESSAGE = Object.freeze({
REQUEST_PAYMENT: "구입금액을 입력해 주세요.\n",
REQUEST_WIN_NUMBER: "\n당첨 번호를 입력해 주세요.\n",
REQUEST_BONUS_NUMBER: "보너스 번호를 입력해 주세요.\n",
REQUEST_RESTART: "다시 시작하시겠습니까? (y/n)",
REQUEST_RESTART: "다시 시작하시겠습니까? (Y / random key) + Enter",
});

export { LOTTO_RULES, LOTTO_PRICE, INFO_MESSAGE };
Loading