Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9520db8
docs(readme): 기능 요구사항 분석
vlmbuyd Nov 2, 2025
3007883
feat(input): 입력값 클래스 구현
vlmbuyd Nov 3, 2025
77751f1
feat(validate): 파사드 패턴을 활용하여 입력값 검증 로직 구현
vlmbuyd Nov 3, 2025
c0f7eff
docs(constant): 주요 매직 스트링 및 매직 넘버 상수화
vlmbuyd Nov 3, 2025
d9c0adc
test(input): 로또 구입 금액 입력값 검증 테스트 코드 작성
vlmbuyd Nov 3, 2025
da9aee7
feat(parse): 파서 클래스 및 구입 가능 티켓 수 계산 메서드 구현
vlmbuyd Nov 3, 2025
438809b
feat(lotto): 로또 발행 클래스 생성 및 관련 로직 구현
vlmbuyd Nov 3, 2025
135c9b2
refactor(lotto): Lotto 클래스 디렉토리 이동 및 예외 메시지 상수화
vlmbuyd Nov 3, 2025
0d3758f
feat(validate): 당첨 번호 입력 검증 기능 구현
vlmbuyd Nov 3, 2025
8e29c27
fix(parse): convertToNumberArray 함수가 파싱 시에 Number('') 값을 0으로 처리하는 버그에…
vlmbuyd Nov 3, 2025
59fe725
test(input): 당첨 번호 입력값 검증 테스트 코드 추가
vlmbuyd Nov 3, 2025
fde8f5a
refactor(validate): 구입 금액 검증 메서드 구조 개선
vlmbuyd Nov 3, 2025
be434ca
feat(input): 보너스 번호 입력 관련 로직 및 테스트코드 추가
vlmbuyd Nov 3, 2025
deb3e6b
refactor(lotto): Lotto 클래스 기능 추가 및 발행된 로또 데이터 구조 변경
vlmbuyd Nov 3, 2025
59b020a
feat(calculate): 랭크당 당첨 횟수 계산 로직 구현
vlmbuyd Nov 3, 2025
4154dc9
feat(output): 당첨 통계 출력 로직 구현
vlmbuyd Nov 3, 2025
cf7772b
feat(constant): 매직 넘버 상수화 및 출력 메시지 상수 개행 추가
vlmbuyd Nov 3, 2025
9f4d022
feat(app): 앱 전체 실행 로직 구현
vlmbuyd Nov 3, 2025
f52ed68
fix(test): 테스트 케이스에 맞게 출력 형식 수정
vlmbuyd Nov 3, 2025
1822d80
docs(readme): 기능 요구사항 업데이트
vlmbuyd Nov 3, 2025
e641f4e
refactor(app): App.run 메서드 리팩토링 및 로직 분리
vlmbuyd Nov 3, 2025
7747feb
chore(test): 입력값 검증 테스트 오타 수정 및 eslint 규칙에 따라 single quote로 변경
vlmbuyd 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
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,67 @@
# javascript-lotto-precourse
## 기능 요구사항 분석

### 입력/출력

**입력**

- [x] 로또 구입 금액을 입력받는다.
- [x] 당첨 번호와 보너스 번호를 입력받는다.
- [x] 잘못된 값을 입력했을 경우 메시지와 함께 에러를 발생시킨 후
해당 지점부터 다시 입력 받는다.

**출력**

- [x] 발행할 로또 개수를 바탕으로 로또 수량 및 번호 리스트들을 출력한다.
- [x] 당첨 내역을 출력한다.
- [x] 수익률을 출력한다.
- [x] 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.

<br>

### 기능

**로또 발행**

- [x] 로또 구입 금액만큼 발행할 로또 개수를 정한다.
- [x] 로또 개수만큼 로또를 발행한다.
- [x] 발행한 로또 번호를 오름차순으로 정렬한다.

**결과 계산**

- [x] 발행된 로또와 당첨 번호를 비교하여 당첨 내역을 계산한다.
- [x] 당첨 내역을 바탕으로 총 수익률을 계산한다.

수익률은 소수점 둘째 자리에서 반올림한다.


<br>

### 에러

**입력값**

- [x] 공백인 경우
- 로또 구입 금액
- [x] 구입 금액의 단위가 1,000으로 나누어 떨어지지 않을 경우
- [x] 숫자가 아닌 문자가 포함된 경우
- 당첨 번호
- [x] 구분자가 쉼표(,)가 아닌 경우
- [x] 숫자가 아닌 문자가 포함된 경우
- [x] 구분자 형식이 잘못된 경우 (e.g. ‘1,2,3,’ ‘1,2,,3’)
- [x] 구분자 사이에 공백이 포함된 경우 (e.g. ‘1, 2,3’ ‘1,2 ,3’)
- [x] 입력된 숫자가 6개가 아닌 경우
- [x] 중복된 번호가 입력된 경우
- [x] 1~45 범위의 숫자가 아닌 경우
- 보너스 번호
- [x] 숫자가 아닌 문자가 포함된 경우

**로또 발행**

- [x] 중복된 번호가 발행된 경우
- [x] 발행된 번호가 6개가 아닌 경우

<br>

### 엣지 케이스

- [x] 당첨되지 않았을 때 (2개 이하로 번호 일치)
Copy link

Choose a reason for hiding this comment

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

체크리스트를 만들고 꼼꼼히 작성하셨네요!!
꼼꼼하신 성격이 코드에도 느껴집니다!

77 changes: 77 additions & 0 deletions __tests__/InputValidatorTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ERROR_MESSAGE, TERMS } from '../src/utils/constants';
import InputValidator from '../src/utils/InputValidator';

describe('입력값 검증 테스트 (로또 구입 금액)', () => {
it.each([
['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT],
[
'숫자가 아닌 문자가 포함된 경우',
'lotto',
ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE,
],
[
'1,000원으로 나누어 떨어지지 않는 경우',
'900',
ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT,
],
])('%s', (_, input, expectedError) => {
expect(() =>
InputValidator.runValidate(TERMS.PURCHASE_AMOUNT, input)
).toThrow(expectedError);
});
});

describe('입력값 검증 테스트 (당첨 번호)', () => {
it.each([
['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT],
[
'구분자가 쉼표(,)가 아닌 경우',
'1,2;3,4,5,6',
ERROR_MESSAGE.WINNING_NUMBER_TYPE,
],
[
'구분자 형식이 잘못된 경우',
',1,2,3,4,5,6',
ERROR_MESSAGE.WINNING_NUMBER_TYPE,
],
[
'숫자가 아닌 문자가 포함된 경우',
'1,2,h,4,5,6',
ERROR_MESSAGE.WINNING_NUMBER_TYPE,
],
[
'숫자가 6개가 아닌 경우',
'1,2,3,4,5',
ERROR_MESSAGE.WINNING_NUMBER_LENGTH,
],
[
'중복된 숫자가 포함된 경우',
'1,2,3,4,5,5',
ERROR_MESSAGE.WINNING_NUMBER_DUPLICATE,
],
[
'1~45 범위 밖의 숫자가 포함된 경우',
'1,2,3,4,5,50',
ERROR_MESSAGE.WINNING_NUMBER_RANGE,
],
])('%s', (_, input, expectedError) => {
expect(() =>
InputValidator.runValidate(TERMS.WINNING_NUMBER, input)
).toThrow(expectedError);
});
});

describe('입력값 검증 테스트 (보너스 번호)', () => {
it.each([
['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT],
[
'숫자가 아닌 문자가 포함된 경우',
'lotto',
ERROR_MESSAGE.BONUS_NUMBER_TYPE,
],
])('%s', (_, input, expectedError) => {
expect(() => InputValidator.runValidate(TERMS.BONUS_NUMBER, input)).toThrow(
expectedError
);
});
});
15 changes: 6 additions & 9 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import Lotto from "../src/Lotto";
import Lotto from '../src/model/Lotto';

describe("로또 클래스 테스트", () => {
test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
describe('로또 클래스 테스트', () => {
test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6, 7]);
}).toThrow("[ERROR]");
}).toThrow('[ERROR]');
});

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

// TODO: 추가 기능 구현에 따른 테스트 코드 작성

Choose a reason for hiding this comment

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

index 9abfb89..622caa7 100644
--- a/__tests__/LottoTest.js
+++ b/__tests__/LottoTest.js
@@ -1,6 +1,8 @@
 import Lotto from '../src/model/Lotto';
 
 describe('로또 클래스 테스트', () => {
+  const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
+
   test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => {
     expect(() => {
       new Lotto([1, 2, 3, 4, 5, 6, 7]);
@@ -12,4 +14,13 @@ describe('로또 클래스 테스트', () => {
       new Lotto([1, 2, 3, 4, 5, 5]);
     }).toThrow('[ERROR]');
   });
+
+  test('로또 번호와 당첨 번호를 비교하여 일치하는 숫자 갯수를 반환합니다.', () => {
+    expect(lotto.calculateMatchCount([4, 5, 6, 7, 8, 9])).toBe(3);
+  });
+
+  test('로또 번호에 보너스 번호가 포함되었는지 확인합니다.', () => {
+    expect(lotto.hasBonusNumber(8)).toBe(false);
+    expect(lotto.hasBonusNumber(3)).toBe(true);
+  });
 });

테스트코드가 더 있다면 좋았을거 같습니다.
예시로 calculateMatchCount, hasBonusNumber 메서드 유닛 테스트를 작성해봤습니다 :)

});
77 changes: 76 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
import { Console } from '@woowacourse/mission-utils';
import LottoMachine from './model/LottoMachine.js';
import { IO_MESSAGE, SEPERATOR, TERMS } from './utils/constants.js';
import Input from './view/Input.js';
import Parser from './utils/Parser.js';
import RankCalculator from './model/RankCalculator.js';
import StatisticsView from './view/StatisticsView.js';

class App {
async run() {}
#issuedLottos = [];
Copy link

Choose a reason for hiding this comment

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

필드로 구현하신 이유가있을까요 ?

run 내부에서 메소드 흐름이 그대로 가서 파라미터로 넘겨주어도 직관적으로 보였을것 같아서요 !


/**
* 1. 구입 금액을 입력받고 숫자로 변환
*/
async getPurchaseAmount() {
const purchaseAmountStr = await Input.readInputValues(
TERMS.PURCHASE_AMOUNT,
IO_MESSAGE.PURCHASE_AMOUNT_INPUT
);
return Number(purchaseAmountStr);
}

/**
* 2. 로또 발행
*/
issueLottos(purchaseAmount) {
this.#issuedLottos = LottoMachine.run(purchaseAmount);
}

/**
* 3. 발행된 로또 번호를 정렬하여 출력
*/
printIssuedLottos() {
this.#issuedLottos.forEach((lotto) => {
const numbers = lotto.getNumbers();
numbers.sort((a, b) => a - b);
Console.print(`[${numbers.join(', ')}]`);
});
}

/**
* 4. 당첨 번호와 보너스 번호를 입력받고 파싱
*/
async getWinningNumbers() {

Choose a reason for hiding this comment

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

getWinningNumbers라는 메서드 이름에서 당첨 번호 배열을 반환하는 것으로 이해를 하고 읽어 내려갔습니다..!
실제로 리턴하는 값은 winningNumber와 bonusNumber 두 값을 가지는 객체를 반환하는 것 같습니다..!

getWinningNumbersAndBonus()와 같이 조금 더 명확한 이름으로 변경해보는 것은 어떠신가요?!

const winningNumber = await Input.readInputValues(
TERMS.WINNING_NUMBER,
IO_MESSAGE.WINNING_NUMBER_INPUT
);
const parsedWinningNumber = Parser.convertToNumberArray(
winningNumber,
SEPERATOR.COMMA
);

const bonusNumber = await Input.readInputValues(
TERMS.BONUS_NUMBER,
IO_MESSAGE.BONUS_NUMBER_INPUT
);

return { winningNumber: parsedWinningNumber, bonusNumber };
}

// 전체 프로세스 실행
async run() {
const purchaseAmount = await this.getPurchaseAmount();
this.issueLottos(purchaseAmount);
this.printIssuedLottos();

const { winningNumber, bonusNumber } = await this.getWinningNumbers();

const rankCounts = RankCalculator.calculate(
this.#issuedLottos,
winningNumber,
bonusNumber
);

StatisticsView.printResult(rankCounts, purchaseAmount);
}
Copy link

Choose a reason for hiding this comment

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

함수의 선언 순서도 중요할수 있다고 들었던것 같습니다..

run 함수가 전반적인 코드의 시작점이라 가장 상단에 존재해야 아무래도 보기 좋을것 같긴합니다 !!

흐름을 읽고 내부 구현을 들여다 보기 마련인데 최하단까지 내려가야하니까요!

}

export default App;
18 changes: 0 additions & 18 deletions src/Lotto.js

This file was deleted.

35 changes: 35 additions & 0 deletions src/model/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ERROR_MESSAGE, LOTTO_RULES } from '../utils/constants.js';

class Lotto {
#numbers;

constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
}

#validate(numbers) {
if (numbers.length !== LOTTO_RULES.TICKET_NUMBER_COUNT) {
throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_COUNT);
}

if (new Set(numbers).size !== numbers.length) {
throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_DUPLICATE);
}
}

getNumbers() {
return [...this.#numbers];
}

calculateMatchCount(winningNumber) {
return this.#numbers.filter((number) => winningNumber.includes(number))
.length;
}

hasBonusNumber(bonusNumber) {
return this.#numbers.includes(Number(bonusNumber));
}
}

export default Lotto;
49 changes: 49 additions & 0 deletions src/model/LottoMachine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Console, MissionUtils } from '@woowacourse/mission-utils';
import { IO_MESSAGE, LOTTO_RULES } from '../utils/constants.js';
import Parser from '../utils/Parser.js';
import Lotto from './Lotto.js';

class LottoMachine {
/**
* 구입 가능한 로또 티켓 수 계산
*/
static #determineLottoNumber(purchaseAmount) {

Choose a reason for hiding this comment

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

determineLottoNumber보단 calculateLottoCount가 더 나을거 같습니다.
LottoNumber라고 했을때는 로또번호와 관련된 로직이 예상됩니다.
갯수를 반환하기 때문에 Count가 더 적절한거 같습니다.

return Parser.getPurchaseCount(purchaseAmount);
}

/**
* 한 개의 로또 번호 생성 (e.g. [8, 21, 23, 41, 42, 43])
*/
static #generateLottoNumbers() {
return MissionUtils.Random.pickUniqueNumbersInRange(
LOTTO_RULES.MIN_NUMBER,
LOTTO_RULES.MAX_NUMBER,
LOTTO_RULES.TICKET_NUMBER_COUNT
);
}

/**
* 로또 티켓 발행
*/
static #issueLottoTickets(purchaseCount) {
const issuedLottos = [];

Array.from({ length: purchaseCount }).forEach(() => {
const lottoNumbers = this.#generateLottoNumbers();
const lotto = new Lotto(lottoNumbers);

issuedLottos.push(lotto);
});

return issuedLottos;
}
Comment on lines +28 to +39

Choose a reason for hiding this comment

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

Suggested change
static #issueLottoTickets(purchaseCount) {
const issuedLottos = [];
Array.from({ length: purchaseCount }).forEach(() => {
const lottoNumbers = this.#generateLottoNumbers();
const lotto = new Lotto(lottoNumbers);
issuedLottos.push(lotto);
});
return issuedLottos;
}
static #issueLottoTickets(purchaseCount) {
const lottos = Array.from(
{ length: purchaseCount },
() => {
const lottoNumbers = this.#generateLottoNumbers();
const lotto = new Lotto(lottoNumbers);
return lotto;
}
);
return lottos;
}

forEach 호출이 불필요한거 같습니다.
그리고 로또번호를 생성하고 로또를 생성하는 과정 또한 분리되었다면 더 좋을거 같습니다.


static run(purchaseAmount) {
const purchaseCount = this.#determineLottoNumber(purchaseAmount);
Console.print(IO_MESSAGE.PURCHASE_COUNT_OUTPUT(purchaseCount));

return this.#issueLottoTickets(purchaseCount);
}
}
Comment on lines +41 to +47

Choose a reason for hiding this comment

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

해당 메서드는 구매 개수를 출력하고, 로또를 반환해주는 동작을 하는 것 같습니다
하나의 함수에서 여러 책임을 가지는 것에서 테스트 하기 어렵지 않을까 싶습니다!

이에 더해서, run이라는 메서드 명이 해당 메서드의 동작을 완전히 설명해주지는 못하는 것 같습니다
저도 좋은 메서드명이 떠오르지는 않는데, 이는 아마 메서드에서 많은 동작을 하는 것이 그 원인이 되지 않을까 싶기도 해요!

출력하는 로직은 해당 클래스가 아니라 다른 곳에서 관리하는 것은 어떠신가요?!


export default LottoMachine;
Loading