Skip to content

Commit

Permalink
[1단계 - 콘솔 기반 로또 게임] 초코(강다빈) 미션 제출합니다. (#290)
Browse files Browse the repository at this point in the history
* docs REQUIREMENTS.md 생성

Co-authored-by: [email protected] <00kang>

* docs: test

* docs: test 하기

테스트중입니다

Co-authored-by: vi-wolhwa <[email protected]>

* docs: 기능 구현 목록 작성

Co-authored-by: vi-wolhwa <[email protected]>

* chore: eslint, prettier 설정 및 package 정보 변경

Co-authored-by: vi-wolhwa <[email protected]>

* test: 구입 금액에 따른 로또 발행 테스트 작성

Co-authored-by: vi-wolhwa <[email protected]>

* chore: eslint 플러그인 재설치

Co-authored-by: 00kang <[email protected]>

* feat: 구입금액에 따른 발행 수량 계산

- 구입금액에 대한 발행 수량 반환
- 구입금액에 대한 유효성검사 (정수, 최솟값)

Co-authored-by: 00kang <[email protected]>

* feat: 랜덤 유틸 클래스 구현

- 특정 범위 내 난수 발생 함수
- 특정 범위 내 조합 추출 함수

Co-authored-by: 00kang <[email protected]>

* feat: 수량 만큼 로또 발행

- 랜덤으로 로또 번호 생성
- 수량 만큼 로또 발행

Co-authored-by: 00kang <[email protected]>

* feat: 로또 객체 생성 및 번호 정렬

- 로또 객체 생성 시 유효성 검사
- 로또 번호 오름차순 정렬

Co-authored-by: 00kang <[email protected]>

* feat: 당첨 결과 분석, 순위 반환

- 로또번호 일치 개수 분석
- 보너스번호 포함 여부 분석
- 로또 결과(순위) 판단

Co-authored-by: 00kang <[email protected]>

* style: printWidth 변경 (80 -> 100)

- 불필요한 줄바꿈 개선

Co-authored-by: 00kang <[email protected]>

* feat: 당첨 결과 분석 및 순위 반환

- 등수 별 당첨 수량 계산
- 당첨 결과에 따른 수익률 계산

Co-authored-by: 00kang <[email protected]>

* refactor: 로또 등수 계산 로직 분리

- 함수 depth 최대 1로 리팩토링

Co-authored-by: 00kang <[email protected]>

* refactor: 상수명 변경

- 복수형으로 변경

Co-authored-by: 00kang <[email protected]>

* feat: 콘솔 입력 구현

- 콘솔 입력 모듈 readLineAsync 작성
- 구입금액, 당첨번호, 보너스번호, 재시작응답 입력

Co-authored-by: 00kang <[email protected]>

* feat: controller, model, view 연결

Co-authored-by: 00kang <[email protected]>

* feat: README.md 수정

* style: 입,출력 메시지의 스타일 수정

* feat: 로또 발행 메서드 컨트롤러에 연결

* fix: 컨트롤러에서 당첨번호 입력 기능의 inputWinningNumbers 메서드 수정

* fix: 당첨 통계 결과 반환 안되는 오류 수정

* fix: 발행된 로또 오름차순 정렬하여 보여주도록 수정

* feat: 실행결과 사진 포함하여 README.md 수정

* feat: 구입 금액에 대한 유효성 검사 진행 및 재입력받도록 추가구현

* feat: 당첨 번호 입력에 대한 유효성 검사 진행 및 재입력받도록 추가구현

* feat: 보너스 점수 입력에 대한 유효성 검사 추가 구현

* feat: 게임 재시작 및 유효성 검사 추가 구현

* feat: REQUIREMENTS.md 업데이트

* refactor: 컨트롤러로 이동

* refactor: 프로그래밍 요구사항에 맞게 eslint 재설치 및 수정

* refactor: eslint max-params 규칙에 따라 수정

* refactor: eslint max-lines-per-function 규칙에 따라 LottoController 내의 메서드 분리

* refacotr: airbnb 컨벤션에 따라 isNan 대신 Number.isNan으로 수정

* refacotr: nullish coalescing operator(??) 사용으로 불필요한 비교 줄임

* refactor: eslint max-params 규칙에 따라 객체로 묶는 수정

* feat:winningNumbers와 bonusNumber를 배열로 반환하여 당첨 결과 출력 오류 수정

* refactor: eslint max-depth 규칙에 따라 메서드 분리

---------

Co-authored-by: unknown <[email protected]>
  • Loading branch information
00kang and vi-wolhwa authored Feb 25, 2024
1 parent d0f3b95 commit b3243f1
Show file tree
Hide file tree
Showing 27 changed files with 9,359 additions and 3,935 deletions.
76 changes: 72 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,79 @@
{
"rules": {},
"env": {
"es6": true,
"node": true
"node": true,
"jest": true
},
"parserOptions": {
"ecmaVersion": "latest"
"ecmaVersion": "latest",
"sourceType": "module"
},
"extends": ["eslint:recommended", "plugin:prettier/recommended"]
"extends": [
"airbnb",
"airbnb/hooks",
"prettier",
"plugin:jest/recommended",
"plugin:prettier/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"eslint:recommended"
],
"plugins": ["import", "jest", "prettier"],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto",
"printWidth": 100
}
],
"prefer-const": [
"error",
{
"destructuring": "any",
"ignoreReadBeforeAssign": false
}
],
"max-depth": ["error", { "max": 1 }],
"max-params": ["error", { "max": 2 }],
"class-methods-use-this": "off",
"max-lines-per-function": ["error", { "max": 10 }],
"import/extensions": ["error", { "js": "ignorePackages" }],
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
["parent", "sibling"],
"index",
"object",
"type",
"unknown"
],
"pathGroups": [
{
"pattern": "next",
"group": "builtin",
"position": "before"
},
{
"pattern": "@/core/**",
"group": "unknown"
},
{
"pattern": "**/*.css.ts",
"group": "unknown",
"position": "after"
}
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
}
}
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always",
"proseWrap": "never",
"endOfLine": "auto"
}
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,61 @@
<h1 align="middle">🎱</h1>
<h2 align="middle">level1 - 행운의 로또</h2>
<p align="middle">자바스크립트로 구현 하는 로또 어플리케이션</p>

### 실행

```bash
npm run start-step1
```

### 테스트

```bash
npm run test
```

### 실행 화면

<img src="./lotto-step1.png" alt="실행화면" width="450">

### 테스트 실행 화면

<img src="./lotto-step1-test.png" alt="테스트 실행화면" width="300">

### 도메인 로직

### 파일 구조

```
📦src
┣ 📂constant : 상수 관리
┃ ┣ 📜Messages.js : 입력, 입력힌트, 출력, 에러 메세지 관리
┃ ┗ 📜Options.js : 로또 가격, 로또 범위 최소, 최대 숫자, 상금, 게임 결과 등 관리
┣ 📂controller
┃ ┗ 📜LottoController.js : 로또 게임 진행
┣ 📂domain
┃ ┣ 📜Lotto.js
┃ ┗ 📜LottoMachine.js
┣ 📂util
┃ ┣ 📂random
┃ ┃ ┗ 📜Random.js
┃ ┣ 📂readLine
┃ ┃ ┗ 📜readLineAsync.js
┃ ┗ 📂validation
┃ ┃ ┣ 📜LottoNumbersValidator.js
┃ ┃ ┣ 📜PurchaseAmountValidator.js
┃ ┃ ┗ 📜Validation.js
┣ 📂view
┃ ┣ 📜InputView.js
┃ ┗ 📜OutputView.js
┣ 📜App.js
┣ 📜step1-index.js
┗ 📜step2-index.js
📦__tests__
┣ 📜.gitkeep
┣ 📜Lotto.test.js
┣ 📜LottoMachine.test.js
┗ 📜Random.test.js
```
102 changes: 102 additions & 0 deletions __tests__/Lotto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable max-lines-per-function */
import { VARIABLE_ALIAS, ERROR_MESSAGES } from '../src/constant/Messages.js';
import OPTIONS from '../src/constant/Options.js';
import Lotto from '../src/domain/Lotto.js';

describe('Lotto 단위테스트', () => {
describe('로또 발행 테스트', () => {
test.each([[[1, 2, 3, 4, 5]], [[1, 2, 3, 4, 5, 6, 7]]])(
'로또 번호(%s)가 6개가 아니면 에러를 발생한다.',
(numbers) => {
expect(() => new Lotto(numbers)).toThrow(
`${ERROR_MESSAGES.prefix}${ERROR_MESSAGES.hasNotLength(VARIABLE_ALIAS.lottoNumbers, OPTIONS.LOTTO.combination)}`
);
}
);

test.each([[['a', 2, 3, 4, 5, 6]], [[1.1, 2, 3, 4, 5, 6]]])(
'로또 번호(%s) 중 정수 이외의 값이 있다면 에러를 발생한다.',
(numbers) => {
expect(() => new Lotto(numbers)).toThrow();
}
);

test.each([[[1, 2, 3, 4, 5, 46]], [[0, 1, 2, 3, 4, 5]]])(
'로또 번호(%s)의 범위가 1부터 45 사이가 아니라면 에러를 발생한다.',
(numbers) => {
expect(() => new Lotto(numbers)).toThrow();
}
);

test('로또 번호에 중복되는 수가 있다면 에러를 발생한다.', () => {
const numbers = [1, 1, 2, 3, 4, 5];

expect(() => new Lotto(numbers)).toThrow();
});

test.each([[[3, 2, 4, 5, 6, 1]], [[13, 12, 14, 15, 16, 11]]])(
'로또 번호는 오름차순으로 정렬된다.',
(numbers) => {
const lotto = new Lotto(numbers);

expect(lotto.getNumbers()).toStrictEqual(numbers.sort());
}
);
});

describe('로또 당첨 여부 판단 테스트', () => {
let lottoNumbers;
let lotto;

beforeAll(() => {
lottoNumbers = [1, 2, 3, 4, 5, 6];
lotto = new Lotto(lottoNumbers);
});

test.each([[[1, 2, 3, 4, 5, 6], 7, 1]])(
'6개의 번호가 일치하면 1등을 반환한다.',
(winningNumbers, bonusNumber, rank) => {
expect(lotto.determineRank(winningNumbers, bonusNumber)).toBe(rank);
}
);

test.each([[[1, 2, 3, 4, 5, 45], 6, 2]])(
'5개의 번호 + 보너스 번호가 일치하면 2등을 반환한다.',
(winningNumbers, bonusNumber, rank) => {
expect(lotto.determineRank(winningNumbers, bonusNumber)).toBe(rank);
}
);

test.each([[[1, 2, 3, 4, 5, 45], 7, 3]])(
'5개의 번호가 일치하고 보너스 번호가 다르면 3등을 반환한다.',
(winningNumbers, bonusNumber, rank) => {
expect(lotto.determineRank(winningNumbers, bonusNumber)).toBe(rank);
}
);

test.each([
[[1, 2, 3, 4, 44, 45], 6, 4],
[[1, 2, 3, 4, 44, 45], 7, 4]
])('4개의 번호가 일치하면 4등을 반환한다.', (winningNumbers, bonusNumber, rank) => {
expect(lotto.determineRank(winningNumbers, bonusNumber)).toBe(rank);
});

test.each([
[[1, 2, 3, 43, 44, 45], 6, 5],
[[1, 2, 3, 43, 44, 45], 7, 5]
])('3개의 번호가 일치하면 5등을 반환한다.', (winningNumbers, bonusNumber, rank) => {
expect(lotto.determineRank(winningNumbers, bonusNumber)).toBe(rank);
});

test.each([
[[1, 2, 42, 43, 44, 45], 6, 6],
[[1, 2, 42, 43, 44, 45], 7, 6],
[[1, 41, 42, 43, 44, 45], 6, 6],
[[1, 41, 42, 43, 44, 45], 7, 6],
[[40, 41, 42, 43, 44, 45], 6, 6],
[[40, 41, 42, 43, 44, 45], 7, 6]
])('3개 미만의 번호가 일치하면 다음기회에^^.', (winningNumbers, bonusNumber, rank) => {
expect(lotto.determineRank(winningNumbers, bonusNumber)).toBe(rank);
});
});
});
88 changes: 88 additions & 0 deletions __tests__/LottoMachine.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable max-lines-per-function */
import { ERROR_MESSAGES, VARIABLE_ALIAS } from '../src/constant/Messages.js';
import OPTIONS from '../src/constant/Options.js';
import Lotto from '../src/domain/Lotto.js';
import LottoMachine from '../src/domain/LottoMachine';

// TODO: calculateIssueQuantity 함수를 private 함수로 변경
describe('LottoMachine 단위테스트', () => {
let lottoMachine;

beforeEach(() => {
lottoMachine = new LottoMachine();
});

describe('구입 금액에 따른 구입 가능 수량 계산 테스트', () => {
test.each(['a', 1.1])(
'구입 금액(%s)이 정수가 아니라면 에러를 발생시킨다.',
(purchaseAmount) => {
expect(() => lottoMachine.calculateIssueQuantity(purchaseAmount)).toThrow(
`${ERROR_MESSAGES.prefix}${ERROR_MESSAGES.isNotInteger(VARIABLE_ALIAS.purchaseAmount)}`
);
}
);

test('구입 금액이 1000 미만이라면 에러를 발생시킨다.', () => {
const purchaseAmount = 999;

expect(() => lottoMachine.calculateIssueQuantity(purchaseAmount)).toThrow(
`${ERROR_MESSAGES.prefix}${ERROR_MESSAGES.isNotAtLeast(VARIABLE_ALIAS.purchaseAmount, OPTIONS.LOTTO.price)}`
);
});

test.each([
[1000, 1],
[1999, 1],
[2000, 2]
])('구입 금액(%s)에 따른 발행 수량(%s)을 계산하여 반환한다.', (purchaseAmount, quantity) => {
expect(lottoMachine.calculateIssueQuantity(purchaseAmount)).toBe(quantity);
});
});

describe('지정 수량 만큼 로또 발행 테스트', () => {
test('지정 수량만큼 로또를 발행한다.', () => {
const issueQuantity = 1;
const lottos = lottoMachine.issueLottos(issueQuantity);

expect(Array.isArray(lottos) && lottos.length === issueQuantity).toBe(true);
});

test('로또 객체의 배열을 반환한다.', () => {
const issueQuantity = 1;
const lottos = lottoMachine.issueLottos(issueQuantity);

expect(lottos.every((lotto) => lotto instanceof Lotto)).toBe(true);
});
});

describe('로또 결과 분석 테스트', () => {
// TEST: 1. 등수 별 당첨 개수
test('구입한 로또들의 등수를 분석하여 반환한다.', () => {
const numbersList = [
[1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 7],
[1, 2, 3, 4, 5, 45],
[1, 2, 3, 4, 44, 45],
[1, 2, 3, 43, 44, 45],
[1, 2, 42, 43, 44, 45]
];
const lottos = numbersList.map((numbers) => new Lotto(numbers));
const winningNumbers = [1, 2, 3, 4, 5, 6];
const bonusNumber = 7;

const winningResult = { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 };

expect(lottoMachine.determineLottoRanks(lottos, winningNumbers, bonusNumber)).toStrictEqual(
winningResult
);
});

// TEST: 2. 수익률 계산
test.each([[{ 1: 0, 2: 0, 3: 0, 4: 0, 5: 1, 6: 7 }, 62.5]])(
'로또 결과에 따른 수익률을 반환한다.',
(winningResult, profitRate) => {
expect(lottoMachine.calculateProfitRate(winningResult)).toBe(profitRate);
}
);
});
});
33 changes: 33 additions & 0 deletions __tests__/Random.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable max-lines-per-function */
import Random from '../src/util/random/Random.js';

describe('랜덤 테스트', () => {
test('특정 범위 내에서 랜덤 값 1개를 반환한다.', () => {
const min = 1;
const max = 10;
const testCount = 10;

const testSuites = Array(testCount)
.fill()
.map(() => Random.randomPickNumber(min, max));

expect(testSuites.every((testSuite) => min <= testSuite && testSuite <= max)).toBe(true);
});

test('특정 범위 내에서 중복되지 않은 조합을 반환한다.', () => {
const min = 1;
const max = 45;
const count = 6;
const testCount = 5;

const testSuites = Array(testCount)
.fill()
.map(() => Random.pickCombination(min, max, count));

expect(
testSuites.every((testSuite) => {
return testSuite.length === new Set(testSuite).size && testSuite.length === count;
})
).toBe(true);
});
});
Loading

0 comments on commit b3243f1

Please sign in to comment.