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

[로또 저장소] 임현우 미션 제출합니다. #393

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da6824c
docs: 초기 README.md 기능 목록 작성
gusn9719 Nov 2, 2024
edfd070
feat: 사용자 메시지 및 상금 정보 상수화
gusn9719 Nov 3, 2024
f2956d9
feat: 콘솔 유틸 함수 모듈화
gusn9719 Nov 3, 2024
bbdcdce
feat: 추가된 에러 메시지 및 명확한 네이밍 변경
gusn9719 Nov 3, 2024
48e6af2
feat: 구매 금액 유효성 검사 추가
gusn9719 Nov 3, 2024
6e6aee8
feat: 구매 금액 입력에 관련된 기능 추가
gusn9719 Nov 3, 2024
41e0096
docs: 로또 구입 금액 입력 및 검증 기능 완료 표시
gusn9719 Nov 3, 2024
649c134
feat: 공백 입력 처리 기능 추가 및 유효성 검사 개선
gusn9719 Nov 4, 2024
869f4f3
test: 로또 구입 금액 입력에 대한 유효성 검사 테스트 케이스 추가
gusn9719 Nov 4, 2024
a3109b3
fix: 구매 금액 유효성 검사 로직 수정
gusn9719 Nov 4, 2024
44b4556
docs: 로또 구입 금액 입력 및 검증 테스트 완료 표시
gusn9719 Nov 4, 2024
d790e93
refactor: 상수 파일 통합 및 구조 개선
gusn9719 Nov 4, 2024
6ecb7f1
feat: 로또 관련 에러 메시지 추가
gusn9719 Nov 4, 2024
432d96c
feat: 로또 발행 기능 구현
gusn9719 Nov 4, 2024
b789fb4
feat: 로또 발행 결과 출력 기능 추가
gusn9719 Nov 4, 2024
665973d
test: 로또 발행 테스트 케이스 추가
gusn9719 Nov 4, 2024
19ef35e
docs: 로또 발행 및 번호 출력 완료 표시
gusn9719 Nov 4, 2024
890a64c
feat: 최대 구입 금액 제한 추가
gusn9719 Nov 4, 2024
91ac8ae
test: 최대 구입 금액 초과에 대한 테스트 케이스 추가
gusn9719 Nov 4, 2024
9b14bb0
feat: 로또 번호 범위와 포맷 검증 관련 상수 추가 및 오류 메시지 개선
gusn9719 Nov 4, 2024
a6d8623
refactor: 숫자 정렬 기능 모듈화
gusn9719 Nov 4, 2024
d1bd6f4
feat: 당첨 번호 유효성 검사 및 입력 처리 기능 추가
gusn9719 Nov 4, 2024
dbab831
refactor: 당첨 번호 유효성 검사 로직 수정 및 파일명 변경
gusn9719 Nov 4, 2024
2717f2a
test: 당첨 번호 입력 유효성 검증 테스트 추가
gusn9719 Nov 4, 2024
07d741a
feat: 에러 메시지 상수 추가
gusn9719 Nov 4, 2024
b79b785
feat: 보너스 번호 입력 기능 추가
gusn9719 Nov 4, 2024
bdfab80
refactor: 당첨 번호 입력 시 숫자 형식 변환 추가
gusn9719 Nov 4, 2024
5308de1
refactor: 당첨 번호 유효성 검증 테스트 수정
gusn9719 Nov 4, 2024
34b04ac
test: 보너스 번호 입력에 대한 유효성 검증 테스트 추가
gusn9719 Nov 4, 2024
eb6a91d
fix: 숫자가 아닌 입력 값 처리 로직 수정
gusn9719 Nov 4, 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: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,77 @@
# javascript-lotto-precourse

## 프로젝트 개요
이 프로젝트는 간단한 로또 발매기를 구현하는 것을 목표로 합니다.
사용자는 로또 구입 금액을 입력하고, 로또 번호를 발행받은 후 당첨 번호와 비교하여 당첨 결과를 확인할 수 있습니다.
프로젝트의 주요 기능은 사용자의 입력을 받아 로또를 발행하고, 당첨 결과를 계산하여 수익률을 계산하는 것입니다.

## 기능 목록

- [x] **로또 구입 금액 입력 및 검증**
- 로또 구입 금액을 입력받는다.
- 1,000원 단위로 나누어 떨어지는지 확인하여 유효성을 검증한다.
- **예외**: 1,000원 단위로 나누어 떨어지지 않는 경우, `[ERROR]` 메시지 출력 후 재입력 요청.

- [x] **로또 발행**
- 입력받은 금액을 바탕으로 로또 발행 개수를 계산한다. (로또 1장당 1,000원)
- 발행한 로또 번호는 1~45 사이의 중복되지 않는 6개의 숫자로 구성된다.

- [x] **로또 번호 출력**
- 발행한 모든 로또 번호를 오름차순으로 정렬하여 출력한다.
- 출력 형식: `N개를 구매했습니다.` 및 각 로또 번호를 배열 형식으로 출력한다.

- [ ] **당첨 번호 및 보너스 번호 입력 및 검증**
- 당첨 번호 6개를 입력받는다. 번호는 쉼표(,)로 구분한다.
- 보너스 번호 1개를 입력받는다.
- 당첨 번호와 보너스 번호는 1 ~ 45 사이의 중복되지 않는 숫자여야 한다.
- **예외**: 입력 형식이 올바르지 않거나 중복된 번호, 범위를 벗어난 경우, `[ERROR]` 메시지 출력 후 재입력 요청.

- [ ] **당첨 결과 계산**
- 구매한 로또 번호와 당첨 번호를 비교하여 당첨 결과를 계산한다.
- 당첨 등수는 1등부터 5등까지 있으며, 각 등수는 아래 조건에 따른다:
- 1등: 6개 번호 일치
- 2등: 5개 번호 + 보너스 번호 일치
- 3등: 5개 번호 일치
- 4등: 4개 번호 일치
- 5등: 3개 번호 일치

- [ ] **당첨 통계 및 수익률 출력**
- 당첨 결과를 등수별로 정리하여 출력한다.
- 총 수익률을 계산하고, 소수점 둘째 자리에서 반올림하여 출력한다.
- 출력 형식:
- `3개 일치 (5,000원) - 1개`
- `총 수익률은 62.5%입니다.`

- [ ] **에러 처리**
- 모든 사용자 입력 단계에서 잘못된 값이 들어왔을 경우, `[ERROR]`로 시작하는 메시지를 출력한다.
- 에러 발생 시 해당 단계부터 다시 입력을 받는다.

## 테스트 목록
TDD를 적용하여 기능별 테스트 케이스를 작성합니다. 다음은 각 기능에 대해 예상되는 테스트 목록입니다.

- [x] **로또 구입 금액 입력 및 검증 테스트**
- 올바른 금액을 입력했을 때 정상적으로 처리되는지 확인한다.
- 1,000원 단위로 나누어 떨어지지 않는 금액을 입력했을 때 에러 메시지가 출력되는지 확인한다.

- [x] **로또 발행 테스트**
- 입력한 금액에 따라 올바른 개수의 로또가 발행되는지 확인한다.
- 각 로또 번호가 1 ~ 45 사이의 중복되지 않는 6개의 숫자로 구성되어 있는지 확인한다.

- [x] **로또 번호 출력 테스트**
- 발행한 로또 번호가 오름차순으로 출력되는지 확인한다.

- [ ] **당첨 번호 및 보너스 번호 입력 및 검증 테스트**
- 올바른 형식의 당첨 번호와 보너스 번호를 입력했을 때 정상적으로 처리되는지 확인한다.
- 중복되거나 범위를 벗어난 번호를 입력했을 때 에러 메시지가 출력되는지 확인한다.

- [ ] **당첨 결과 계산 테스트**
- 구매한 로또 번호와 당첨 번호를 비교하여 각 등수별로 당첨 결과가 정확하게 계산되는지 확인한다.
- 보너스 번호를 포함한 2등 당첨이 올바르게 계산되는지 확인한다.

- [ ] **당첨 통계 및 수익률 출력 테스트**
- 당첨 결과가 등수별로 올바르게 출력되는지 확인한다.
- 총 수익률이 올바르게 계산되어 출력되는지 확인한다.

- [ ] **에러 처리 테스트**
- 각 단계에서 잘못된 입력이 주어졌을 때 올바르게 에러 메시지가 출력되고, 재입력을 받는지 확인한다.

26 changes: 26 additions & 0 deletions __tests__/bonus-number-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import validateBonusNumber from '../src/validations/bouns-number.js';
import { ERROR_MESSAGES } from '../src/constants/constants.js';
const { INVALID_NUMBER_INPUT, INVALID_BONUS_NUMBER, INVALID_LOTTO_NUMBER } =
ERROR_MESSAGES;

describe('보너스 번호 입력 테스트', () => {
const testArray = [1, 2, 3, 4, 5, 6];

test('숫자가 아닌 값을 입력했을 때 에러 메시지가 발생하는지 확인', () => {
expect(() => validateBonusNumber('a', testArray)).toThrow(
INVALID_NUMBER_INPUT,
);
});

test('중복된 번호를 입력했을 때 에러 메시지가 발생하는지 확인', () => {
expect(() => validateBonusNumber(3, testArray)).toThrow(
INVALID_BONUS_NUMBER,
);
});

test('범위를 벗어난 번호를 입력했을 때 에러 메시지가 발생하는지 확인', () => {
expect(() => validateBonusNumber(46, testArray)).toThrow(
INVALID_LOTTO_NUMBER,
);
});
});
34 changes: 34 additions & 0 deletions __tests__/generate-lotto-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import generateLottoList from '../src/utils/generate-lotto.js';
import { LOTTO_TICKET_PRICE } from '../src/constants/constants.js';

describe('로또 발행 테스트', () => {
const amount = 5000;
const lottos = generateLottoList(amount);

test('입력한 금액에 따라 올바른 개수의 로또가 발행되는지 확인한다', () => {
const expectedLottoCount = amount / LOTTO_TICKET_PRICE;

expect(lottos.length).toBe(expectedLottoCount);
});

test('각 로또 번호가 1 ~ 45 사이의 중복되지 않는 6개의 숫자로 구성되어 있는지 확인한다', () => {
lottos.forEach((lotto) => {
const numbers = lotto.getNumbers();

expect(numbers.length).toBe(6);
expect(new Set(numbers).size).toBe(6);
numbers.forEach((number) => {
expect(number).toBeGreaterThanOrEqual(1);
expect(number).toBeLessThanOrEqual(45);
});
});
});

test('각 로또 번호가 오름차순으로 정렬되어 있는지 확인한다', () => {
lottos.forEach((lotto) => {
const numbers = lotto.getNumbers();
const sortedNumbers = [...numbers].sort((a, b) => a - b);
expect(numbers).toEqual(sortedNumbers);
});
});
});
40 changes: 40 additions & 0 deletions __tests__/purchase-amount-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import validateAmount from '../src/validations/purchase-amount.js';
import { ERROR_MESSAGES } from '../src/constants/constants.js';

const {
INVALID_EMPTY_INPUT,
INVALID_PURCHASE_AMOUNT_NOT_NUMBER,
INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT,
INVALID_PURCHASE_AMOUNT_LIMIT,
} = ERROR_MESSAGES;

describe('로또 구입 금액 입력 테스트', () => {
test.each([1000, 8000, 14000])(
'올바른 금액 %i을 입력시 기능 테스트',
(amount) => {
const result = validateAmount(amount);
expect(result).toBe(amount);
},
);

test.each([
{ input: '', expectedError: INVALID_EMPTY_INPUT },
{ input: '100j', expectedError: INVALID_PURCHASE_AMOUNT_NOT_NUMBER },
{ input: 'a', expectedError: INVALID_PURCHASE_AMOUNT_NOT_NUMBER },
{ input: '@#!', expectedError: INVALID_PURCHASE_AMOUNT_NOT_NUMBER },
{ input: '4500', expectedError: INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT },
{ input: '400', expectedError: INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT },
])(
'잘못된 입력 값 %s을 입력시 상응하는 에러 메시지 %s 발생',
({ input, expectedError }) => {
expect(() => validateAmount(input)).toThrow(expectedError);
},
);

test('구입 금액이 최대 금액을 초과할 경우 예외가 발생한다', () => {
const invalidAmount = 150000; // 최대 금액을 초과하는 금액
expect(() => validateAmount(invalidAmount)).toThrow(
INVALID_PURCHASE_AMOUNT_LIMIT,
);
});
});
53 changes: 53 additions & 0 deletions __tests__/winning-numbers-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import validateWinningNumbers from '../src/validations/winning-numbers.js';
import { ERROR_MESSAGES } from '../src/constants/constants.js';

const { INVALID_FORMAT, INVALID_LOTTO_NUMBER, INVALID_DUPLICATE_NUMBERS } =
ERROR_MESSAGES;

describe('당첨 번호 입력 테스트', () => {
test.each([
[
'올바른 형식의 당첨 번호를 입력할 때 정상적으로 처리되는지 확인',
'1,2,3,4,5,6',
[1, 2, 3, 4, 5, 6],
],
])('%s', (_, input, expected) => {
const result = validateWinningNumbers(input);
expect(result).toEqual(expected);
});

test.each([
[
'숫자 형식이 아닌 경우 에러 메시지가 발생하는지 확인',
'1,2,a,4,5,6',
INVALID_FORMAT,
],
[
'구분자가 , 가 아닌 경우 에러 메시지가 발생하는지 확인',
'1/2/3/4/5/6',
INVALID_FORMAT,
],
[
'6개의 숫자가 아닌 경우 에러 메시지가 발생하는지 확인',
'1,2,3,4,5',
INVALID_FORMAT,
],
[
'중복된 번호가 있는 경우 에러 메시지가 발생하는지 확인',
'1,2,3,4,5,5',
INVALID_DUPLICATE_NUMBERS,
],
[
'범위를 벗어난 번호가 있는 경우 에러 메시지가 발생하는지 확인 (0 포함)',
'0,2,3,4,5,6',
INVALID_LOTTO_NUMBER,
],
[
'범위를 벗어난 번호가 있는 경우 에러 메시지가 발생하는지 확인 (46 포함)',
'1,2,3,4,5,46',
INVALID_LOTTO_NUMBER,
],
])('%s', (_, input, expectedError) => {
expect(() => validateWinningNumbers(input)).toThrow(expectedError);
});
});
12 changes: 11 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { printMessage, printLottoList } from './utils/console.js';
import getPurchaseAmount from './utils/get-parchase-amount.js';
import generateLottoList from './utils/generate-lotto.js';

class App {
async run() {}
async run() {
const purchaseAmount = await getPurchaseAmount();

const lottos = generateLottoList(purchaseAmount);

printLottoList(lottos);
}
}

export default App;
20 changes: 19 additions & 1 deletion src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { ERROR_MESSAGES } from './constants/constants.js';

const { INVALID_DUPLICATE_NUMBERS, INVALID_LOTTO_NUMBER_COUNT } =
ERROR_MESSAGES;

class Lotto {
#numbers;

Expand All @@ -8,11 +13,24 @@ class Lotto {

#validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
throw new Error(INVALID_LOTTO_NUMBER_COUNT);
}

this.#checkUniqueNumbers(numbers);
}

// TODO: 추가 기능 구현
#checkUniqueNumbers(numbers) {
const uniqueNumbers = new Set(numbers);

if (uniqueNumbers.size !== numbers.length) {
throw new Error(INVALID_DUPLICATE_NUMBERS);
}
}

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

export default Lotto;
59 changes: 59 additions & 0 deletions src/constants/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const LOTTO_TICKET_PRICE = 1000;
const MAX_PURCHASE_AMOUNT = 100000;

const LOTTO_NUMBER_RANGE = {
MIN_NUMBER: 1,
MAX_NUMBER: 45,
};

const USER_PROMPT_MESSAGES = Object.freeze({
GET_PURCHASE_AMOUNT: '구입금액을 입력해 주세요.\n',
GET_WINNING_NUMBER: '\n당첨 번호를 입력해 주세요.\n',
GET_BONUS_NUMBER: '\n보너스 번호를 입력해 주세요.\n',
});

const CONFIRMATION_MESSAGES = Object.freeze({
PURCHASE_COMPLETE_MESSAGE: (count) => `${count}개를 구매했습니다.`,
WINNING_STATISTICS_HEADER: '당첨 통계\n---',
TOTAL_YIELD_MESSAGE: (rate) => `총 수익률은 ${rate}%입니다.`,
});

const ERROR_MESSAGES = Object.freeze({
INVALID_EMPTY_INPUT:
'[ERROR] 아무것도 입력하지 않았습니다. 값을 입력해 주세요.',
INVALID_PURCHASE_AMOUNT_LIMIT:
'[ERROR] 구입 금액은 최대 100,000원까지 가능합니다.',
INVALID_PURCHASE_AMOUNT_NOT_NUMBER:
'[ERROR] 구입 금액은 숫자만 입력해 주세요.',
INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT:
'[ERROR] 구입 금액은 1,000원 단위로 입력해 주세요.',
INVALID_LOTTO_NUMBER_COUNT: '[ERROR] 로또 번호는 6개여야 합니다.',
INVALID_DUPLICATE_NUMBERS: '[ERROR] 로또 번호는 중복되지 않아야 합니다.',
INVALID_FORMAT:
'[ERROR] 당첨 번호는 콤마(,)로 구분된 숫자 6개여야 합니다. (예: 1,2,3,4,5,6)',
INVALID_LOTTO_NUMBER: '[ERROR] 로또 번호는 1~45 사이의 숫자여야 합니다.',
INVALID_BONUS_NUMBER:
'[ERROR] 보너스 번호는 당첨 번호와 중복되지 않아야 합니다.',
INVALID_NUMBER_INPUT: '[ERROR] 숫자만 입력해 주세요',
});

const PRIZE_AMOUNTS = Object.freeze({
FIRST_PRIZE: 2000000000,
SECOND_PRIZE: 30000000,
THIRD_PRIZE: 1500000,
FOURTH_PRIZE: 50000,
FIFTH_PRIZE: 5000,
});

const WINNING_NUMBER_FORMAT_REGEX = /^(\d{1,2})(,\d{1,2}){5}$/;

export {
LOTTO_TICKET_PRICE,
USER_PROMPT_MESSAGES,
CONFIRMATION_MESSAGES,
ERROR_MESSAGES,
PRIZE_AMOUNTS,
MAX_PURCHASE_AMOUNT,
LOTTO_NUMBER_RANGE,
WINNING_NUMBER_FORMAT_REGEX,
};
25 changes: 25 additions & 0 deletions src/utils/console.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Console } from '@woowacourse/mission-utils';
import { CONFIRMATION_MESSAGES } from '../constants/constants.js';

const { PURCHASE_COMPLETE_MESSAGE } = CONFIRMATION_MESSAGES;

const printMessage = (message) => {
Console.print(message);
};

const promptUserInput = async (message) => {
return await Console.readLineAsync(message);
};

const printLotto = (lotto) => {
Console.print(`[${lotto.getNumbers().join(', ')}]`);
};

const printLottoList = (lottos) => {
printMessage(`\n${PURCHASE_COMPLETE_MESSAGE(lottos.length)}`);
lottos.forEach((lotto) => {
printLotto(lotto);
});
};

export { printMessage, promptUserInput, printLottoList };
Loading