Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
24c80bd
docs: update requirements in
junepil Oct 30, 2025
af93439
chore: setup husky and eslint
junepil Oct 31, 2025
28857c8
docs: add reason why our program doesn't need MVC pattern
junepil Oct 31, 2025
6a51790
feat: implement `Lotto.js` class and unit test
junepil Oct 31, 2025
a8ce284
docs: add centralized validator in design
junepil Oct 31, 2025
123d0f8
feat: implement `Validator.js` class and test
junepil Oct 31, 2025
97d233e
refactor: use `Validator.js` inside the `Lotto.js`
junepil Oct 31, 2025
8de57fe
feat: implement custom error class
junepil Oct 31, 2025
4fa4cbd
chore: add `index.js` for util
junepil Oct 31, 2025
99e8cec
feat: add lotto related contants
junepil Oct 31, 2025
a47255a
feat: add mutipleOf method in `Validator.js`
junepil Nov 1, 2025
f3f64c1
refactor: extract validation function in `Lotto.js`
junepil Nov 1, 2025
10f8173
feat: add class validation to `Validator.js`
junepil Nov 2, 2025
e76b247
feat: implement error for LottoVender and LottoStatistician
junepil Nov 2, 2025
b367001
chore: move constants to seperate directory
junepil Nov 2, 2025
75571b1
feat: implement `LottoVender.js` class
junepil Nov 2, 2025
9170090
test: add tc for Lotto test
junepil Nov 3, 2025
e6e2251
feat: implement `LottoStatistician.js`
junepil Nov 3, 2025
d80a192
feat: add barrel file for model
junepil Nov 3, 2025
224f791
test: implement test for `LottoStatistician.js`
junepil Nov 3, 2025
cc34dd5
chore: rename path using `.js` in error barrel file
junepil Nov 3, 2025
21c97a0
chore: modify import using barrel in `LottoVender.test.js`
junepil Nov 3, 2025
9229a6d
test: add custom class validation tc for `Validator.js`
junepil Nov 3, 2025
acbb430
test: move unit test of Lotto to `__test__` due to the requirement
junepil Nov 3, 2025
acc8754
feat: add message for `LottoVender.js` error
junepil Nov 3, 2025
42fbe4a
feat: implement for IO
junepil Nov 3, 2025
c04e0dc
style: format predefined files using prettier to follow style convention
junepil Nov 3, 2025
a7c41a4
fix: change import statement to include `.js`
junepil Nov 3, 2025
ac28ee3
refactor: tailor output string for better readablity in `CliLottoView…
junepil Nov 3, 2025
3528039
fix: add profit rate float conversion for output
junepil Nov 3, 2025
0560210
feat: implement `App.js` to run
junepil Nov 3, 2025
98f01ba
test: edit wrong test value
junepil Nov 3, 2025
88131d2
fix: edit output format for test
junepil Nov 3, 2025
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
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,101 @@
# javascript-lotto-precourse
# javascript-률lotto-precourse

우아한 테트코스 8기 프리코스 3주차 미션인 로또 추첨기를 구현한 레포지토리입니다.

`@woowacourse/mission-utils` 라이브러리와 `jest`를 활용해 읽기 쉽고 유지보수가 용이한 코드를 작성하는 걸 목표로 하고 있습니다.

## Requirements

### Error

- 모든 에러는 `'[ERROR]'` 문자열을 포함하는 메시지 출력
- 입력에 대한 에러가 발생했을 때 문자열을 출력하고 재입력

### Input

- 로또 구매 금액 입력
- 1,000원 단위로 입력, 나누어 떨어지지 않을 시 에러
```sh
구입금액을 입력해 주세요.
8000
```
- 당첨 번호와 보너스 번호 입력
- 요구사항에는 당첨 번호와 보너스 번호를 뽑는다고 나와있지만 입출력 요구사항은 당첨 번호와 보너스 번호를 입력받는다고 나와있다
- 중복 번호 입력 시 에러

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

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

### Core logic

- 새로운 로또를 발행
- 1~45 사이의 조합
- 한 장당 금액은 1,000원
- 번호는 오름차순으로 정렬해 저장
```sh
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등 | 6개 숫자 모두 일치 | 2,000,000,000원 |
| 2등 | 5개 번호 + 보너스 번호 일치 | 30,000,000원 |
| 3등 | 5개 번호 일치 | 1,500,000원 |
| 4등 | 4개 번호 일치 | 50,000원 |
| 5등 | 3개 번호 일치 | 5,000원 |

- 수익률 계산
- 소수 둘째 자리에서 반올림해 수익금/투자금을 계산

### Output

- 발권한 모든 로또에 대한 정보
- 당첨 통계
- 각 등수에 해당하는 로또 개수
- 총 수익
```sh
당첨 통계
---
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%입니다.
```

## Design

값 검증을 위해 중앙화된 클래스를 생성해서 사용하자.

값 검증은 로또 생성, 구매 금액 그리고 당첨 번호 입력 시에 사용되므로 재사용성이 요구된다.

## Thoughts

MVC는 왜 필요할까?

최초에 MVC 패턴이 등장하게 된 이유는 살아있는 GUI 프로그램을 구현하기 위함이었다.
GUI로 사용자의 입력을 받고, 메모리를 업데이트하고 영속화하는 과정에서 Synchronisation을 일관되게 유지하기 때문이다.

현재 과제를 개발함에 있어서 동적인 UI 변경은 전혀 요구되지 않기 때문에, MVC는 필요하지 않다.

다만 그 정신인 관심사의 분리는 여전히 타당하다.
현실 객체를 모델링하는 객체들은 그 자체가 UI에 의존성을 전혀 가지지 않고 동작할 수 있어야 한다.

## References

- https://martinfowler.com/eaaDev/uiArchs.html
50 changes: 25 additions & 25 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import App from "../src/App.js";
import { MissionUtils } from "@woowacourse/mission-utils";
import App from '../src/App.js';
import { MissionUtils } from '@woowacourse/mission-utils';

const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();
Expand All @@ -19,7 +19,7 @@ const mockRandoms = (numbers) => {
};

const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, "print");
const logSpy = jest.spyOn(MissionUtils.Console, 'print');
logSpy.mockClear();
return logSpy;
};
Expand All @@ -29,7 +29,7 @@ const runException = async (input) => {
const logSpy = getLogSpy();

const RANDOM_NUMBERS_TO_END = [1, 2, 3, 4, 5, 6];
const INPUT_NUMBERS_TO_END = ["1000", "1,2,3,4,5,6", "7"];
const INPUT_NUMBERS_TO_END = ['1000', '1,2,3,4,5,6', '7'];

mockRandoms([RANDOM_NUMBERS_TO_END]);
mockQuestions([input, ...INPUT_NUMBERS_TO_END]);
Expand All @@ -39,15 +39,15 @@ const runException = async (input) => {
await app.run();

// then
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]"));
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[ERROR]'));
};

describe("로또 테스트", () => {
describe('로또 테스트', () => {
beforeEach(() => {
jest.restoreAllMocks();
});

test("기능 테스트", async () => {
test('기능 테스트', async () => {
// given
const logSpy = getLogSpy();

Expand All @@ -61,37 +61,37 @@ describe("로또 테스트", () => {
[2, 13, 22, 32, 38, 45],
[1, 3, 5, 14, 22, 45],
]);
mockQuestions(["8000", "1,2,3,4,5,6", "7"]);
mockQuestions(['8000', '1,2,3,4,5,6', '7']);

// when
const app = new App();
await app.run();

// then
const logs = [
"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]",
"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%입니다.",
'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]',
'3개 일치 (5,000원) - 1개',
'4개 일치 (50,000원) - 0개',
'5개 일치 (1,500,000원) - 0개',
'5개 일치, 보너스 볼 일치 (30,000,000원) - 0개',
'6개 일치 (2,000,000,000원) - 0개',
'총 수익률은 0.6%입니다.',
];

logs.forEach((log) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log));
});
});

test("예외 테스트", async () => {
await runException("1000j");
test('예외 테스트', async () => {
await runException('1000j');
});
});
38 changes: 30 additions & 8 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import Lotto from "../src/Lotto";
import { Lotto } from '../src/model/index.js';
import { LottoNumberError } from '../src/error/index.js';

describe("로또 클래스 테스트", () => {
test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
describe('Lotto', () => {
it.each([[[10, 20, 30, 5, 16, 27]], [[1, 2, 3, 4, 5, 6]]])(
'saves %s in ascending order',
(numbers) => {
const lotto = new Lotto(numbers);

expect(lotto.getNumbers()).toEqual(numbers.sort((a, b) => a - b));
},
);

it('creates error if input length is more than 6.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6, 7]);
}).toThrow("[ERROR]");
}).toThrow(LottoNumberError);
});

// TODO: 테스트가 통과하도록 프로덕션 코드 구현
test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
it('creates error if there is duplicated number.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 5]);
}).toThrow("[ERROR]");
}).toThrow(LottoNumberError);
});

it('creates error if some number is not between 1 to 45.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 46]);
}).toThrow(LottoNumberError);
});

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
it.each([[[1, 2, 3, 4, 5, 1.5]], [[1, 2, 3, 4, 5, 'one']]])(
'creates error if some input is non-integer in %s.',
(arg) => {
expect(() => {
new Lotto(arg);
}).toThrow(LottoNumberError);
},
);
});
17 changes: 17 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import js from '@eslint/js';
import globals from 'globals';
import { defineConfig } from 'eslint/config';

export default defineConfig([
{
files: ['**/*.{js,mjs,cjs}'],
plugins: { js },
extends: ['js/recommended'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
},
},
]);
Loading