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

[로또] 진찬용 미션 제출합니다. #384

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
93c45ff
docs: README 작성
Jin-Chanyong Nov 4, 2024
713620a
feat: 에러 발생 및 출력 함수 구현
Jin-Chanyong Nov 4, 2024
c55ce6b
docs: README 파일 수정
Jin-Chanyong Nov 4, 2024
a732530
feat: 구매 금액 입력값 유효성 검사 함수 추가
Jin-Chanyong Nov 4, 2024
ea95ba0
refactor: 에러 발생 함수 변경
Jin-Chanyong Nov 4, 2024
c23310f
feat: 구입 금액 입력 함수 추가
Jin-Chanyong Nov 4, 2024
da014af
fix: import 경로에 확장자 추가
Jin-Chanyong Nov 4, 2024
cfcd9b5
feat: 당첨 번호 유효성 검사 함수 추가
Jin-Chanyong Nov 4, 2024
d2e052c
feat: 보너스 점수 입력값 유효성 검사 함수 추가
Jin-Chanyong Nov 4, 2024
451c3a1
feat: 당첨 번호 입력 함수 추가
Jin-Chanyong Nov 4, 2024
760082f
feat: 보너스 번호 입력 함수 추가
Jin-Chanyong Nov 4, 2024
a015064
fix: getValidatedWinningNumbers 함수 입력 메시지 수정
Jin-Chanyong Nov 4, 2024
1fc221b
feat: Lotto 클래스 구현
Jin-Chanyong Nov 4, 2024
aa9b571
feat: 로또 관련 상수 파일 추가
Jin-Chanyong Nov 4, 2024
e8f7497
feat: 로또 생성 함수 추가
Jin-Chanyong Nov 4, 2024
9c0d75c
feat: 로또 출력 함수 추가
Jin-Chanyong Nov 4, 2024
8334275
feat: 로또 번호의 일치 개수를 반환하는 함수 추가
Jin-Chanyong Nov 4, 2024
f521865
feat: 수익률을 계산하는 함수 추가
Jin-Chanyong Nov 4, 2024
1f1adc9
feat: 로또 등수별 당첨 횟수 반환 함수 추가
Jin-Chanyong Nov 4, 2024
4597a64
feat: 로또 당첨금의 수익률을 계산하는 함수 추가
Jin-Chanyong Nov 4, 2024
cab8c1d
feat: 로또 결과 출력 함수 추가
Jin-Chanyong Nov 4, 2024
ea620ca
feat: 로또 발매기 로직 구현
Jin-Chanyong Nov 4, 2024
5a43376
fix: Lotto 클래스 import 문 추가
Jin-Chanyong Nov 4, 2024
9e446b2
fix: getValidatedBonusNumber import 문 추가
Jin-Chanyong Nov 4, 2024
0535fac
fix: printLottos 함수 출력값 수정
Jin-Chanyong Nov 4, 2024
e749ecd
fix: bonusNumberValidate 오류 수정
Jin-Chanyong Nov 4, 2024
549d473
fix: 테스트와 출력값 불일치 오류 수정
Jin-Chanyong Nov 4, 2024
a185696
fix: Lotto 클래스 테스트 오류 수정
Jin-Chanyong Nov 4, 2024
d60d283
feat: 로또 테스트 케이스 추가
Jin-Chanyong Nov 4, 2024
8848994
docs: 테스트 결과에 따른 기능 목록 단위 체크
Jin-Chanyong 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
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,47 @@
# javascript-lotto-precourse
# javascript-racingcar-precourse

## 기능 요구 사항

- 로또 번호의 숫자 범위는 1~45까지이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
- 1등: 6개 번호 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원
- 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
- 로또 1장의 가격은 1,000원이다.
- 당첨 번호와 보너스 번호를 입력받는다.
- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
- 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 해당 지점부터 다시 입력을 받는다.

## 기능 목록 단위

- [x] 1. 로또 구매 로직 구현

- [x] 1-1. 사용자로부터 로또 구입 금액 입력받기
- [x] 입력된 금액이 1000원 단위로 나누어 떨어지지 않거나, 잘못된 입력 시 다시 입력.
- [x] 1-2. 구입 금액에 따라 구매할 로또 수량을 계산하기
- [x] 1-3. 구매한 로또 수량 만큼 로또 번호 생성하고 출력하기
- [x] 로또 번호는 오름차순으로 정렬.

- [x] 2. 당첨 번호 및 보너스 번호 입력 로직 구현

- [x] 2-1. 사용자로부터 당첨 번호 6개와 보너스 번호 1개를 입력받기
- [x] 입력된 번호가 1부터 45사이의 정수가 아니거나, 중복이 있거나, 쉼표로 구분되지 않을 시 다시 입력.

- [x] 3. 당첨 내역 비교 및 결과 계산

- [x] 3-1. 구매한 로또 번호와 당첨 번호를 비교하여 당첨 등수 계산하기
- [x] 3-2. 1등부터 5등까지의 당첨 횟수 출력하기

- [x] 4. 수익률 계산 및 출력

- [x] 4-1. 전체 당첨 금액 계산하기.
- [x] 4-2. 수익률을 계산해 소수점 둘째 자리에서 반올림하여 출력하기.

- [x] 5. 테스트 코드 작성

- [x] Jest 를 이용하여, 위 1, 2, 3, 4 가 제대로 기능하는지, 출력값은 정확한지를 테스트한다.
17 changes: 6 additions & 11 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { MissionUtils } from "@woowacourse/mission-utils";

const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();

MissionUtils.Console.readLineAsync.mockImplementation(() => {
const input = inputs.shift();

return Promise.resolve(input);
});
};
Expand All @@ -25,30 +23,25 @@ const getLogSpy = () => {
};

const runException = async (input) => {
// given
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"];

mockRandoms([RANDOM_NUMBERS_TO_END]);
mockQuestions([input, ...INPUT_NUMBERS_TO_END]);

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

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

describe("로또 테스트", () => {
describe("로또 애플리케이션 테스트", () => {
beforeEach(() => {
jest.restoreAllMocks();
});

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

mockRandoms([
Expand All @@ -63,11 +56,9 @@ describe("로또 테스트", () => {
]);
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]",
Expand All @@ -91,7 +82,11 @@ describe("로또 테스트", () => {
});
});

test("예외 테스트", async () => {
test("예외 테스트 - 유효하지 않은 금액 입력", async () => {
await runException("1000j");
});

test("예외 테스트 - 유효하지 않은 당첨 번호 입력", async () => {
await runException("1,2,3,4,5,abc");
});
});
2 changes: 1 addition & 1 deletion __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Lotto from "../src/Lotto";
import Lotto from "../models/lotto.js";

describe("로또 클래스 테스트", () => {
test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
Expand Down
14 changes: 14 additions & 0 deletions constants/lottoConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const lotto = {
PRICE: 1000,
NUMBER_COUNT: 6,
NUMBER_MIN: 1,
NUMBER_MAX: 45,
};

export const prizeMoney = {
FIRST: 2000000000,
SECOND: 30000000,
THIRD: 1500000,
FOURTH: 50000,
FIFTH: 5000,
};
18 changes: 18 additions & 0 deletions models/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { winningNumbersValidate } from "../modules/inputValidator.js";

export default class Lotto {
#numbers;

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

#validate(numbers) {
winningNumbersValidate(numbers.join(","));
}

getNumbers() {
return this.#numbers;
}
}
74 changes: 74 additions & 0 deletions modules/inputValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import throwError from "../utils/throwError.js";

export function purchaseAmountValidate(amount) {
if (!amount.trim()) {
throwError("구입 금액을 입력해 주세요.");
}

if (isNaN(amount)) {
throwError("구입 금액은 숫자로만 입력해 주세요.");
}

const amountNumber = Number(amount);

if (amountNumber <= 0 || amountNumber % 1000 !== 0) {
throwError("구입 금액은 1,000원 단위의 양의 정수로 입력해 주세요.");
}
}

export function winningNumbersValidate(numbers) {
const numbersArray = numbers.split(",");
const numbersSet = new Set(numbersArray);

if (!numbers.trim()) {
throwError("번호를 입력해 주세요.");
}

if (numbersArray.length !== 6) {
throwError("로또 번호는 6개여야 합니다.");
}

if (numbersSet.size !== numbersArray.length) {
throwError("중복된 번호가 있습니다.");
}

numbersArray.forEach((number) => {
const numericValue = Number(number);

if (isNaN(number)) {
throwError("잘못된 입력이거나, 쉼표로 구분된 숫자열이 아닙니다.");
}

if (!Number.isInteger(numericValue)) {
throwError("번호는 정수로 입력해주세요.");
}

if (numericValue < 1 || numericValue > 45) {
throwError("숫자는 1 과 45 사이로 입력해주세요.");
}
});
}

export function bonusNumberValidate(number, pickedNumbers) {
const numericValue = Number(number);

if (!number.trim()) {
throwError("번호를 입력해 주세요.");
}

if (isNaN(number)) {
throwError("번호는 숫자로 입력해 주세요.");
}

if (!Number.isInteger(numericValue)) {
throwError("번호는 정수로 입력해주세요.");
}

if (numericValue < 1 || numericValue > 45) {
throwError("숫자는 1 과 45 사이로 입력해주세요.");
}

if (pickedNumbers.some((number) => number === numericValue)) {
throwError("이전에 선택한 숫자와 중복된 숫자입니다.");
}
}
115 changes: 115 additions & 0 deletions modules/lottoService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Console, Random } from "@woowacourse/mission-utils";
import { lotto, prizeMoney } from "../constants/lottoConstants.js";
import { calculateYield, getMatchedCount } from "../utils/lottoUtils.js";
import Lotto from "../models/lotto.js";

export function generateLottos(count) {
const lottos = [];

for (let lottoCount = 0; lottoCount < count; lottoCount++) {
const numbers = Random.pickUniqueNumbersInRange(
lotto.NUMBER_MIN,
lotto.NUMBER_MAX,
lotto.NUMBER_COUNT
);

numbers.sort((a, b) => a - b);

lottos.push(new Lotto(numbers));
}

return lottos;
}

export function printLottos(lottos) {
Console.print(`\n${lottos.length}개를 구매했습니다.`);

lottos.forEach((lotto) => {
Console.print(`[${lotto.getNumbers().join(", ")}]`);
});
}

function calculateWinsCount(lottos, winningNumbers, bonusNumber) {
const resultCount = {
FIRST: 0,
SECOND: 0,
THIRD: 0,
FOURTH: 0,
FIFTH: 0,
};

lottos.forEach((lotto) => {
const matchedCount = getMatchedCount(lotto.getNumbers(), winningNumbers);
const hasBonus = lotto.getNumbers().includes(bonusNumber);

if (matchedCount === 6) {
resultCount.FIRST += 1;
return;
}

if (matchedCount === 5 && hasBonus) {
resultCount.SECOND += 1;
return;
}

if (matchedCount === 5) {
resultCount.THIRD += 1;
return;
}

if (matchedCount === 4) {
resultCount.FOURTH += 1;
return;
}

if (matchedCount === 3) {
resultCount.FIFTH += 1;
return;
}
});

return resultCount;
}

function calculateLottoYield(resultCount, purchaseAmount) {
const totalPrize =
resultCount.FIRST * prizeMoney.FIRST +
resultCount.SECOND * prizeMoney.SECOND +
resultCount.THIRD * prizeMoney.THIRD +
resultCount.FOURTH * prizeMoney.FOURTH +
resultCount.FIFTH * prizeMoney.FIFTH;

return calculateYield(totalPrize, purchaseAmount);
}

export function printLottoResult(
lottos,
winningNumbers,
bonusNumber,
purchaseAmount
) {
const resultCount = calculateWinsCount(lottos, winningNumbers, bonusNumber);
const lottoYield = calculateLottoYield(resultCount, purchaseAmount);

Console.print("\n당첨 통계\n---");
Console.print(
`3개 일치 (${prizeMoney.FIFTH.toLocaleString()}원) - ${resultCount.FIFTH}개`
);
Console.print(
`4개 일치 (${prizeMoney.FOURTH.toLocaleString()}원) - ${
resultCount.FOURTH
}개`
);
Console.print(
`5개 일치 (${prizeMoney.THIRD.toLocaleString()}원) - ${resultCount.THIRD}개`
);
Console.print(
`5개 일치, 보너스 볼 일치 (${prizeMoney.SECOND.toLocaleString()}원) - ${
resultCount.SECOND
}개`
);
Console.print(
`6개 일치 (${prizeMoney.FIRST.toLocaleString()}원) - ${resultCount.FIRST}개`
);
Console.print(`총 수익률은 ${lottoYield}%입니다.`);
}
51 changes: 51 additions & 0 deletions modules/userInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Console } from "@woowacourse/mission-utils";
import {
bonusNumberValidate,
purchaseAmountValidate,
winningNumbersValidate,
} from "./inputValidator.js";

export async function getValidatedPurchaseAmount() {
while (true) {
const purchaseAmount = await Console.readLineAsync(
"구입 금액을 입력해 주세요.\n"
);

try {
purchaseAmountValidate(purchaseAmount);
return purchaseAmount;
} catch (error) {
Console.print(error.message);
}
}
}

export async function getValidatedWinningNumbers() {
while (true) {
const winningNumbers = await Console.readLineAsync(
"\n당첨 번호를 입력해 주세요.\n"
);

try {
winningNumbersValidate(winningNumbers);
return winningNumbers.split(",").map(Number);
} catch (error) {
Console.print(error.message);
}
}
}

export async function getValidatedBonusNumber(winningNumbers) {
while (true) {
const bonusNumber = await Console.readLineAsync(
"\n보너스 번호를 입력해 주세요.\n"
);

try {
bonusNumberValidate(bonusNumber, winningNumbers);
return Number(bonusNumber);
} catch (error) {
Console.print(error.message);
}
}
}
Loading