Skip to content

Commit 0d1262a

Browse files
authored
[2단계 - 행운의 로또 미션] 피터(박규성) 미션 제출합니다. (#386)
* docs: 요구사항 작성 * feat: 상수 설정 * refactor: 폴더명 수정 * feat: 로또 금액 입력받아 계산 * feat: 로또 숫자 생성 * feat: 유저의 로또 번호와 랜덤 로또 번호 비교 * feat: 당첨번호와 보너스번호 입력 * feat: app 호출 * feat: 로또 등수 구하기 * feat: 수익 계산과 수익률 계산 * feat: 미션 요구사항에 맞게 OutputView 분리 * feat: 로또 번호 중복 체크 * feat: 로또 오름차순 정렬 * feat: retryUntilSuccess 구현 * feat: 로또 게임 유효성 검사 * feat: 로또 구매 개수 출력 * refactor: 각 책임에 맞게 LottoCompany 분리 * refactor: calculateProfitRate 유틸 분리 * refactor: calculateTotalProfit 분리 * refactor: 상수 분리 및 불필요한 if문 제거 * refactor: y, n 상수 분리 * refactor: calculateMatchCount 유틸 분리 * refactor: generateUniqueNumberArray 로직 수정 * refactgor: 불필요한 인자 제거 * fix: cur -> prev로 수정 * rerfactor: 불필요한 파일 제거 * test: 오타 제거 및 테스트 코드 축약 * feat: eslint, prettier 설정 * style: prettier, eslint 포매팅 * style: 1depth 프로그래밍 요구사항 준수 * style: 매개변수 2개 이하 프로그래밍 요구사항 준수 * refactor: 유효성 검사 로직 분리 * refactor: Lotto의 필드 private으로 변경 * refactor: 유틸 함수 내 뷰 의존 분리 * refactor: 주석 및 콘솔 제거 * fix: 로또 순위 결정 로직 수정 * refactor: 로또 개수 구할 때 소수점 이하 버림 * refactor: 정적 메서드 호출에 클래스명 사용 - 보다 명시적으로 정적 메서드 호출임을 표현 * refactor: export default class 컨벤션으로 수정 * refactor: 함수 선언문을 화살표 함수 표현식으로 통일 * test: 애플리케이션 테스트 코드 작성 * feat: coverage 스크립트 추가 * test: 전체 출력에 대한 테스트 코드 * fix: 프로퍼티명 오타 수정 및 `원` 출력 메시지 추가 * refactor: 모킹 모듈화 * feat: 애플리케이션 테스트 코드의 setup 및 teardown * test: 각 기능 요구사항 테스트 케이스 작성 * test: 각 예외 사항에 맞는 테스트 케이스 * refactor: NO, YES 상수화 * test: 재시작에 대한 테스트 코드 작성 * refactor: 개수 및 숫자 상수화 * refactor: `OUTPUT_MESSAGES` 제거 이유 : 동적으로 변경되는 값이 많아 사용성이 낮다. * refactor: index.js로 묶어 export * test: 로또 객체에 대한 테스트 케이스 작성 * test: LottoCompany와 LottoShop의 테스트 코드 * refactor: 적절한 네이밍으로 수정 * chore: package.json > homepage 수정 * feat: 웹 뼈대 작성 * refactor: innerHMTL을 활용해 리팩토링 * feat: 당첨 결과 화면 구현 * feat: 스타일링 * fix: 이미 구매한 로또가 있다면 실행하지 않음 * feat: 모달 구현 * refactor: 색상 상수화 * refactor: 각 렌더링 로직을 함수로 모듈화 * refactor: 돔 관련 유틸 분리 * refactor: 뷰 로직 분리 * refactor: OutputView.#print로 화면 출력 관련 로직 응집 * refactor: 상수 활용해 리팩토링 * feat: 뼈대 만드기 * chore: 스타일링 * refactor: WebOutputView에서 초기 html 렌더링 * refactor: App으로 로직 분리 * refactor: container 렌더링과 이후 로직 분리 * refactor: css 파일 분리 * refactor: 공통된 css 속성 분리 * refactor: 디자인 시스템 코드 분리 * chore: 컨테이너 요소로 감싸기 * refactor: class, id 값 문자열로 사용 이유 : class와 id를 상수화하면 유지보수가 더 어려움. (CSS에서 상수를 사용하기 어려움) * refactor: web과 cli의 폴더 분리 * refactor: 함수 선언형으로 코드 순서 변경 * refactor: 뷰 파일을 web폴더로 분리 * refactor: Namespace BEM으로 클래스명 통일 * design: 스타일 다른 부분 수정 * feat: 유효성 검사 * refactor: __container같은 중복된느 키워드 제거 * fix: utils에서 readline 로직 제거 * fix: 경로 수정 * feat: 모달 닫는 버튼 * style: 템플릿 리터럴 형식 통일 * refactor: print에서 render로 수정 * refactor: 클래스의 메서드로 분리 * refactor: 금액을 넘기면 로또를 반환 * design: container의 고정된 높이 제거 * refactor: 시맨틱 태그에 맞게 수정 * feat: 제출 후 버튼 비활성화 * feat: EXC 혹은 배경 클릭 시 모달 닫기 * refactor: 로직 모듈화 * refactor: 이벤트 위임으로 한 곳에서 이벤트 리스너를 관리 * refactor: 에러 처리를 한 곳에서 관리 * refactor: 중괄호 제거 * refactor: remove로 제거하는 로직 간결화 * refactor: 불필요한 return 제거 * refactor: 이벤트 리스너를 한 번만 등록하도록 메서드 분리 * refactor: 콘솔 제거 * refactor: 새로운 div로 감싸지 않도록 기존 innerHTML에 더하기 * refactor: 버튼 클릭을 이벤트 위임으로 수정 * refactor: 모든 이벤트 리스너를 App에서 관리 * design: 다른 디자인 수정 * feat: scss도 git에 업로드 * refactor: 미사용 유틸 제거 * refactor: 다시 시작을 OutputView에서 호출 * refactor: 사용하지 않는 Modal 컴포넌트 제거 * chore: innerHTML에서 insertAdjacentHTML로 수정 * feat: header, footer 높이만큼 간격 * feat: 로또 구매 시 input도 disabled * feat: my-modal 커스텀 요소 구현 * refactor: 모달 제거하는 로직 수정 * fix: 오타 제거 * refactor: MyModal로 수정 * refactor: valueAsNumber로 숫자로 값 얻기 * chore: 기본값 제거 * docs: 주석으로 css에 대한 설명 추가 * refactor: import만 수행 * refactor: call 바인딩 제거 * refactor: 삼항 연산자 대신 filter로 리팩토링 * refactor: `formatMessage` 유틸 이용해 리팩토링 * fix: showModal 로직 제거 * feat: jest context 세팅 * test: context를 활용해 테스트 코드 구체적으로 작성 * test: test를 it으로 수정 * docs: 테스트 설명글 수정 * refactor: INPUT_MESSAGES 상수 활용 * refactor: cli를 command로 수정 * refactor: 중복 파일 제거 * fix: 빠진 reutrn 추가 * refactor: 중복 파일 제거 * feat: github actions로 배포 * docs: 리드미 업데이트 * chore: deploy.yml 수정 * fix: 오타 제거 * feat: 배포 환경 설정 * fix: vite 실행되지 않는 이슈 * refactor: 로또 당첨 결과를 Map으로 관리
1 parent 39d4632 commit 0d1262a

43 files changed

Lines changed: 1585 additions & 284 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.json

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,12 @@
88
"sourceType": "module"
99
},
1010
"rules": {
11-
"no-process-exit": "warn",
12-
"no-console": "off",
13-
"import/extensions": [
14-
"warn",
15-
"never",
16-
{
17-
"js": "always"
18-
}
19-
],
20-
"no-constant-condition": "off",
21-
"func-style": ["off"],
22-
2311
"lines-between-class-members": ["warn", "always", { "exceptAfterSingleLine": true }],
24-
"no-unused-vars": "off",
25-
"import/no-cycle": "off",
26-
"no-new": "off",
27-
"no-restricted-syntax": "off",
28-
"import/prefer-default-export": "off",
29-
"no-return-await": "off",
30-
"simple-import-sort/imports": "warn",
31-
"simple-import-sort/exports": "warn",
32-
"no-empty-function": "off",
33-
"class-methods-use-this": "off",
34-
"no-useless-constructor": "off",
35-
"max-len": "off",
36-
"no-continue": "off",
37-
"max-params": ["warn", 2],
38-
"max-depth": ["warn", 1]
12+
// 프로그래밍 요구사항
13+
"functional/no-let": "error",
14+
"max-depth": ["error", 1],
15+
"max-params": ["error", 2],
16+
"max-lines-per-function": ["error", 10]
3917
},
4018
"settings": {
4119
"react": {

.github/workflows/deploy.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Deploy static content to Pages
2+
3+
on: push
4+
5+
permissions:
6+
contents: read
7+
pages: write
8+
id-token: write
9+
10+
concurrency:
11+
group: 'pages'
12+
cancel-in-progress: true
13+
14+
jobs:
15+
deploy:
16+
runs-on: ubuntu-latest
17+
environment:
18+
name: github-pages
19+
url: ${{ steps.deployment.outputs.page_url }}
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Node
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: 20
28+
cache: 'npm'
29+
- name: Use Cache
30+
uses: actions/cache@v3
31+
id: cache
32+
with:
33+
path: '**/node_modules'
34+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
35+
restore-keys: |
36+
${{ runner.os }}-node-
37+
38+
- name: Install dependencies
39+
run: npm ci
40+
if: steps.cache.outputs.cache-hit != 'true'
41+
42+
- name: Build
43+
run: npm run build
44+
- name: Setup Pages
45+
uses: actions/configure-pages@v4
46+
- name: Upload artifact
47+
uses: actions/upload-pages-artifact@v3
48+
with:
49+
path: './dist'
50+
- name: Deploy to GitHub Pages
51+
id: deployment
52+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
26+
coverage
27+
*.css.map

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,87 @@
5454
다시 시작하시겠습니까? (y/n)
5555
```
5656
- [예외] y/n이 아닐 경우
57+
58+
## 프로젝트 소개
59+
60+
web/App.js에서 핵심 로직을 담당하고 있습니다. App.js에는 크게 두 가지의 메서드가 있습니다.
61+
62+
1. 초기 렌더링 : header, footer, 그리고 로또 구매 폼같은 정적인 요소를 렌더링합니다.
63+
2. 이벤트 리스너 바인딩 : 로또 구매 폼을 눌렀을 때, 그리고 그 이후 렌더링되는 요소의 이벤트까지 동적인 요소를 담당합니다.
64+
65+
뷰 로직은 InputView.js와 OutputView.js에서 담당하며 App.js에서 이를 호출합니다.
66+
67+
1. web/InputView.js : 문서에서 필요한 input요소를 찾아서 값을 가져와 이를 반환합니다.
68+
2. web/OutputView.js : 문서에 요소를 추가한다. 이 때 container요소에 새로운 요소를 추가합니다.
69+
70+
### 가장 신경 쓴 부분
71+
72+
1. Step2를 진행하며 가장 신경 쓴 부분은 각 객체의 책임입니다. App.js는 정적 렌더링과 이벤트 바인딩을, Output.js는 사용자에게 보여주는 화면을, Input.js는 사용자로부터 입력받은 데이터를 가져오는 역할을 수행한ㄷ
73+
74+
## 리뷰 요청 & 논의하고 싶은 내용
75+
76+
<!-- PR 작성자로서, 코드만으로는 알기 어려운 작성자의 의도와 문제 해결 과정에 대해 공유해 주세요.
77+
78+
이 PR에서 달성해야 하는 학습 목표를 잘 달성하고 있는지 스스로 확인하며, 피드백 받을 수 있는 내용일지 점검해 보세요.
79+
리뷰어에게 정답을 묻기보다 고민하고 의사 결정한 과정에 대해 공유하고 이에 대한 피드백을 받으며 대화해보기를 권장합니다. -->
80+
81+
### 1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점
82+
83+
<!-- 구현 과정에서 가장 어려웠던 점이나 많이 고민한 점은 무엇인가요?
84+
이를 해결하기 위해 어떤 방법들을 검토하고 시도했으며, 그 과정에서 새롭게 배운 점이 있나요? -->
85+
86+
1. 콜백 패턴 때문에 가독성이 떨어진다.
87+
88+
프로그램 흐름 순서는 다음과 같습니다.
89+
90+
사용자가 구매 금액을 입력하면 → 구매한 로또를 보여주고, 로또 번호를 입력하면 → 결과를 보여준다.
91+
92+
순차적인 방식이기에 이벤트 핸들러를 폼에 달고, 그 다음 보여주는 폼에 이벤트 핸들러는 방식으로 구현을 할 수밖에 없다는 생각을 했습니다. 이 이벤트 핸들러를 어떻게 하면 조금이라도 더 가독성을 좋게 할 수 있을까 하다가 JavaScript의 함수 호이스팅을 활용해서 함수를 더 뒤에 작성하는 방식으로 작성을 했습니다. 이벤트 핸들러에서 함수를 전달하고, 그 다음 함수를 작성하는 방식입니다.
93+
94+
이렇게 함으로써 조금이라도 가독성을 개선시킬 수 있을 것이라 생각했습니다. 그럼에도 콜백 함수에서 콜백 함수를 호출하는 것이기에 가독성이 여전히 떨어진다고 느꼈습니다.
95+
96+
⇒ 이를 App.js클래스의 상태 관리와 메서드 분리를 통해서 개선을 했습니다.
97+
98+
⇒ window에 이벤트 리스너를 등록하는 이벤트 위임을 통해, 이전 요소의 렌더링 여부와 관계없이 이벤트 리스너를 등록할 수 있어 호출(attach..)만 하면 됩니다. 이를 통해 콜백 패턴의 문제를 해결했습니다.
99+
100+
2. 컴포넌트 단위로 나누어야할까?
101+
102+
리액트를 사용한다면 페이지를 컴포넌트 단위로 나누고, 컴포넌트를 조합해 코드를 작성했을 것입니다. 하지만 현재 작성한 코드의 구조는 컴포넌트 단위로 나누기보다는, App.js 내에서 OutputView를 호출해 필요한 요소를 브라우저에 추가하는 방식으로 구현이 되었습니다. 그래서 클래스형 기반의 컴포넌트를 만들고, 이를 조합해 사용하는 방식으로 구현할까 고민을 했습니다.
103+
104+
하지만 이번 로또 2단계 과제 구현의 목표는 `UI와 도메인 영역을 분리할 수 있는 설계`와 `목적에 맞게 객체와 함수를 활용`, `단위 테스트 기반으로 점진적인 리팩터링`이었습니다. 이러한 목표에 컴포넌트로 나누고 이를 조합하는 방식은 목표와 떨어진다고 판단했습니다. 그래서 이번 과제제의 목표를 이루는데 중점을 두었습니다.
105+
106+
UI와 도메인 영역은 각각 InputView, OutputView, 그리고 도메인 영역은 스텝 1에서 구현한 도메인 로직을 활용했습니다.
107+
108+
3. DOM을 직접 만들고 삽입하는게 맞을까?
109+
110+
처음에는 일일이 document.createElement로 요소를 생성하고 이를 삽입하는 방식으로 구현을 했습니다다. 이렇게 구현을 하니 가독성이 매우 떨어지는 것이 느껴졌습니다. 전체적인 구조가 전혀 보이지 않았습니다. 예를 들어 아래와 같습니다.
111+
112+
```jsx
113+
const todoItem = document.createElement('li');
114+
const checkbox = document.createElement('input');
115+
checkbox.type = 'checkbox';
116+
checkbox.className = 'todo-checkbox';
117+
118+
todoItem.appendChild(checkbox);
119+
todoItem.appendChild(todoText);
120+
```
121+
122+
위와 같은 방식은 너무 명령적이고, 가독성이 떨어진다고 판단했습니다. 그래서 아래와 같이 innerHTML을 활용한 코드로 수정했습니다.
123+
124+
```jsx
125+
todoList.innerHTML += `
126+
<li>
127+
<input type="checkbox" class="todo-checkbox">
128+
</li>
129+
`;
130+
```
131+
132+
4. index.html에 초기 html이 있는게 맞을까?
133+
134+
처음에 보여주는 html을 index.html에 작성했습니다. 그리고 그 이후에는 돔을 javascript로 추가하는 방식이었습니다. 그런데 이 방식이 과연 더 적절할까 고민을 했습니다. 초반에 html이 있다는 말은, 그 요소들은 html에 기본적으로 있다는 것을 의미합니다. 물론 이 또한 JavaScript를 이용해 동적으로 수정이 가능하며 있어도 무방합니다.
135+
136+
그럼에도 javascript로 전부 동적으로 수정할 수 있다고 개발자에게 알리기 위해 모든 요소를 javascript로 삽입하는 방식을 채택했습니다.
137+
138+
5. class와 id는 어느 상황에 써야할까?
139+
140+
처음에는 id와 class를 혼합해서 사용해 혼란스러웠습니다. 일관된 규칙이 없었습니다. 그래서 이 둘의 규칙을 만들어야겠다고 생각했고, id는 페이지 내에서 유일한 app, container같은 요소에만 사용했고 그 외에는 class를 사용했습니다.

__tests__/Application.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import App from '../src/command/App.js';
2+
import InputView from '../src/command/views/InputView.js';
3+
import { ERROR_MESSAGES, NO, YES } from '../src/lib/constants';
4+
import * as utils from '../src/lib/utils.js';
5+
6+
const mockReadLineAsync = (mockValues) => {
7+
mockValues.forEach((mockValue) => jest.spyOn(InputView, 'readLineAsync').mockResolvedValueOnce(mockValue));
8+
};
9+
10+
const mockGenerateUniqueNumbers = (mockValues) => {
11+
mockValues.forEach((mockValue) => jest.spyOn(utils, 'generateUniqueNumbers').mockReturnValueOnce(mockValue));
12+
};
13+
14+
describe('Application', () => {
15+
let consoleLogSpy;
16+
beforeEach(() => {
17+
consoleLogSpy = jest.spyOn(console, 'log');
18+
});
19+
afterEach(() => {
20+
jest.restoreAllMocks();
21+
});
22+
23+
describe('기능 요구사항 테스트', () => {
24+
it('정상적인 경우의 출력을 테스트한다.', async () => {
25+
mockReadLineAsync(['5000', '1,2,3,4,5,6', '7', NO]);
26+
mockGenerateUniqueNumbers([
27+
[1, 2, 3, 4, 5, 6],
28+
[1, 2, 3, 4, 5, 11],
29+
[1, 2, 3, 4, 11, 12],
30+
[1, 2, 3, 11, 12, 13],
31+
[1, 2, 11, 12, 13, 14],
32+
]);
33+
34+
const app = new App();
35+
await app.run();
36+
37+
[
38+
'5개를 구매했습니다.',
39+
[1, 2, 3, 4, 5, 6],
40+
[1, 2, 3, 4, 5, 11],
41+
[1, 2, 3, 4, 11, 12],
42+
[1, 2, 3, 11, 12, 13],
43+
[1, 2, 11, 12, 13, 14],
44+
'3개 일치 (5,000원) - 1개',
45+
'4개 일치 (50,000원) - 1개',
46+
'5개 일치 (1,500,000원) - 1개',
47+
'5개 일치, 보너스 볼 일치 (30,000,000원) - 0개',
48+
'6개 일치 (2,000,000,000원) - 1개',
49+
'총 수익률은 40031100%입니다.',
50+
].forEach((expectedConsoleLogMessage) => {
51+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
52+
});
53+
});
54+
55+
it('로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.', async () => {
56+
mockReadLineAsync(['1000', '1,2,3,4,5,6', '7', NO]);
57+
58+
const app = new App();
59+
await app.run();
60+
61+
['1개를 구매했습니다.'].forEach((expectedConsoleLogMessage) => {
62+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
63+
});
64+
});
65+
66+
it('로또 번호는 오름차순으로 정렬하여 보여준다.', async () => {
67+
mockReadLineAsync(['1000', '1,2,3,4,5,6', '7', NO]);
68+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
69+
70+
const app = new App();
71+
await app.run();
72+
73+
[[1, 2, 3, 4, 5, 6]].forEach((expectedConsoleLogMessage) => {
74+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
75+
});
76+
});
77+
78+
it('재시작시 게임을 다시 시작한다.', async () => {
79+
mockReadLineAsync(['1000', '1,2,3,4,5,6', '7', YES, '1000', '1,2,3,4,5,6', '7', NO]);
80+
mockGenerateUniqueNumbers([
81+
[1, 2, 3, 4, 5, 7],
82+
[1, 2, 3, 4, 5, 8],
83+
]);
84+
85+
const app = new App();
86+
await app.run();
87+
88+
[
89+
[1, 2, 3, 4, 5, 7],
90+
[1, 2, 3, 4, 5, 8],
91+
].forEach((expectedConsoleLogMessage) => {
92+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
93+
});
94+
});
95+
});
96+
97+
describe('예외 사항 처리', () => {
98+
describe('구입금액', () => {
99+
it('구입 금액은 양의 정수여야한다.', async () => {
100+
mockReadLineAsync(['0', '1000', '1,2,3,4,5,6', '7', NO]);
101+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
102+
103+
const app = new App();
104+
await app.run();
105+
106+
[ERROR_MESSAGES.purchaseAmount.positiveInteger].forEach((expectedConsoleLogMessage) => {
107+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
108+
});
109+
});
110+
it('구입 금액은 1000으로 나누어 떨어져야한다.', async () => {
111+
mockReadLineAsync(['10', '1000', '1,2,3,4,5,6', '7', NO]);
112+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
113+
114+
const app = new App();
115+
await app.run();
116+
117+
[ERROR_MESSAGES.purchaseAmount.thousandUnit].forEach((expectedConsoleLogMessage) => {
118+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
119+
});
120+
});
121+
});
122+
123+
describe('당첨 번호', () => {
124+
it('당첨 번호는 중복되지 않은 숫자여야한다.', async () => {
125+
mockReadLineAsync(['1000', '1,2,3,4,5,5', '1,2,3,4,5,6', '7', NO]);
126+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
127+
128+
const app = new App();
129+
await app.run();
130+
131+
[ERROR_MESSAGES.winNumber.unique].forEach((expectedConsoleLogMessage) => {
132+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
133+
});
134+
});
135+
test.each(['1,2,3,4,5,6,7', '1,2,3,4,5', '1,2,3,4,5,46', '0,1,2,3,4,5'])(
136+
'당첨 번호은 6개의 1-45 사이의 정수여야한다.',
137+
async (value) => {
138+
mockReadLineAsync(['1000', value, '1,2,3,4,5,6', '7', NO]);
139+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
140+
141+
const app = new App();
142+
await app.run();
143+
144+
[ERROR_MESSAGES.winNumber.range].forEach((expectedConsoleLogMessage) => {
145+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
146+
});
147+
},
148+
);
149+
});
150+
151+
describe('보너스 번호', () => {
152+
it('보너스 번호는 당첨 번호와 중복되면 안된다.', async () => {
153+
mockReadLineAsync(['1000', '1,2,3,4,5,6', '6', '7', NO]);
154+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
155+
156+
const app = new App();
157+
await app.run();
158+
159+
[ERROR_MESSAGES.bonusNumber.unique].forEach((expectedConsoleLogMessage) => {
160+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
161+
});
162+
});
163+
test.each(['0', '46', 'a', '1,2'])('보너스 번호는 1개의 1-45 사이의 정수여야한다.', async (value) => {
164+
mockReadLineAsync(['1000', '1,2,3,4,5,6', value, '7', NO]);
165+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
166+
167+
const app = new App();
168+
await app.run();
169+
170+
[ERROR_MESSAGES.bonusNumber.range].forEach((expectedConsoleLogMessage) => {
171+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
172+
});
173+
});
174+
});
175+
176+
describe('재시작 여부', () => {
177+
test.each(['Y', 'N', 'Of Course! Why Not?'])(`재시작 여부는 ${YES} 또는 ${NO}이어야한다.`, async (value) => {
178+
mockReadLineAsync(['1000', '1,2,3,4,5,6', '7', value, NO]);
179+
mockGenerateUniqueNumbers([[3, 2, 5, 4, 1, 6]]);
180+
181+
const app = new App();
182+
await app.run();
183+
184+
[ERROR_MESSAGES.retry.yesOrNo].forEach((expectedConsoleLogMessage) => {
185+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedConsoleLogMessage);
186+
});
187+
});
188+
});
189+
});
190+
});

0 commit comments

Comments
 (0)