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

[1단계 - 콘솔 기반 로또 게임] 초코(강다빈) 미션 제출합니다. #290

Merged
merged 39 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9bb5a9e
docs REQUIREMENTS.md 생성
vi-wolhwa Feb 20, 2024
656df6c
docs: test
00kang Feb 20, 2024
85feae0
docs: test 하기
00kang Feb 20, 2024
a1f4355
docs: 기능 구현 목록 작성
00kang Feb 20, 2024
e8c8231
chore: eslint, prettier 설정 및 package 정보 변경
00kang Feb 20, 2024
812e848
test: 구입 금액에 따른 로또 발행 테스트 작성
00kang Feb 20, 2024
8010ff6
chore: eslint 플러그인 재설치
vi-wolhwa Feb 21, 2024
725f690
feat: 구입금액에 따른 발행 수량 계산
vi-wolhwa Feb 21, 2024
916057e
feat: 랜덤 유틸 클래스 구현
vi-wolhwa Feb 21, 2024
d90568f
feat: 수량 만큼 로또 발행
vi-wolhwa Feb 21, 2024
6e11789
feat: 로또 객체 생성 및 번호 정렬
vi-wolhwa Feb 21, 2024
f689337
feat: 당첨 결과 분석, 순위 반환
vi-wolhwa Feb 22, 2024
3885553
style: printWidth 변경 (80 -> 100)
vi-wolhwa Feb 22, 2024
41e3459
feat: 당첨 결과 분석 및 순위 반환
vi-wolhwa Feb 22, 2024
be26777
refactor: 로또 등수 계산 로직 분리
vi-wolhwa Feb 22, 2024
e7aac4b
refactor: 상수명 변경
vi-wolhwa Feb 22, 2024
f9d2c32
feat: 콘솔 입력 구현
vi-wolhwa Feb 22, 2024
03ee900
feat: controller, model, view 연결
vi-wolhwa Feb 22, 2024
993ff65
feat: README.md 수정
00kang Feb 22, 2024
7cf4395
style: 입,출력 메시지의 스타일 수정
00kang Feb 22, 2024
6804840
feat: 로또 발행 메서드 컨트롤러에 연결
00kang Feb 22, 2024
ea9485a
fix: 컨트롤러에서 당첨번호 입력 기능의 inputWinningNumbers 메서드 수정
00kang Feb 22, 2024
a1eb586
fix: 당첨 통계 결과 반환 안되는 오류 수정
00kang Feb 23, 2024
21e5888
fix: 발행된 로또 오름차순 정렬하여 보여주도록 수정
00kang Feb 23, 2024
ff23a3a
feat: 실행결과 사진 포함하여 README.md 수정
00kang Feb 23, 2024
e1572cf
feat: 구입 금액에 대한 유효성 검사 진행 및 재입력받도록 추가구현
00kang Feb 23, 2024
de926bc
feat: 당첨 번호 입력에 대한 유효성 검사 진행 및 재입력받도록 추가구현
00kang Feb 23, 2024
788cbf0
feat: 보너스 점수 입력에 대한 유효성 검사 추가 구현
00kang Feb 23, 2024
cb70b91
feat: 게임 재시작 및 유효성 검사 추가 구현
00kang Feb 23, 2024
184aacf
feat: REQUIREMENTS.md 업데이트
00kang Feb 23, 2024
ed22aa4
refactor: 컨트롤러로 이동
00kang Feb 23, 2024
27c944d
refactor: 프로그래밍 요구사항에 맞게 eslint 재설치 및 수정
00kang Feb 23, 2024
1b86786
refactor: eslint max-params 규칙에 따라 수정
00kang Feb 23, 2024
b573410
refactor: eslint max-lines-per-function 규칙에 따라 LottoController 내의 메서드 분리
00kang Feb 24, 2024
c681e4a
refacotr: airbnb 컨벤션에 따라 isNan 대신 Number.isNan으로 수정
00kang Feb 24, 2024
dfe9bb0
refacotr: nullish coalescing operator(??) 사용으로 불필요한 비교 줄임
00kang Feb 24, 2024
e7b65d9
refactor: eslint max-params 규칙에 따라 객체로 묶는 수정
00kang Feb 24, 2024
979af80
feat:winningNumbers와 bonusNumber를 배열로 반환하여 당첨 결과 출력 오류 수정
00kang Feb 24, 2024
f501d39
refactor: eslint max-depth 규칙에 따라 메서드 분리
00kang Feb 24, 2024
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
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": [

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 import order까지 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 프로그래밍 요구사항에 맞게 eslint를 설정했다고 생각했는데 제대로 적용되고 있지 않는 점
  2. 이전 미션의 프로그래밍 요구 사항은 기본으로 포함해야 하는 점
    위의 두 가지 이유로 eslint 설정을 다시 해보고자 합니다!

"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