Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,48 @@
# javascript-lotto-precourse

## 기능 목록

1. 입력 기능(InputView)

- 구입금액 입력받기
- 숫자 형식 검증
- 양수 검증
- 1000원 단위 검증
- 당첨 번호 입력받기
- 쉼표로 구분된 6개 숫자 입력
- 1~45범위 검증
- 중복 번호 검증
- 보너스 번호 입력받기
- 숫자 형식 검증
- 1~45범위 검증

2. 로또 생성기능 (LottoMake, Lotto)

- 로또 번호 자동 생성
- 1~45 범위에서 중복 없이 6개 숫자 추출
- 오름차순 정렬
- 로또 객체 생성 및 검증
- 로또 번호 개수 검증
- 중복 번호 검증
- 번호 범위 검증
- 구입 금액에 따른 로또 발행 개수 계산
- 1,000원당 1개 발행

3. 당첨 확인 기능 (WinningResult)

- 각 로또의 당첨 여부 확인
- 당첨 번호와 일치하는 개수 계산
- 보너스 번호 포함 여부 확인
- 당첨 등수 판정
- 당첨 통계 집계
- 총 수익금 계산
- 수익률 계산

4. 출력 기능 (OutputView)

- 구매한 로또 개수 출력
- 발행된 로또 번호 출력
- 당첨 통계 출력
- 각 등수별 당첨 개수
- 각 등수별 당첨 금액
- 총 수익률 출력
48 changes: 48 additions & 0 deletions __tests__/InputView.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Console } from "@woowacourse/mission-utils";
import InputView from "../src/InputView";

jest.mock("@woowacourse/mission-utils", () => ({
Console: {
readLineAsync: jest.fn(),
print: jest.fn(),
},
}));

describe("InputView 클래스 테스트", () => {
let inputView;
beforeEach(() => {
inputView = new InputView();
jest.clearAllMocks();
});
describe("purchaseAmountInput", () => {
test("양수가 아니면 에러가 발생한다.", async () => {
Console.readLineAsync.mockResolvedValue("-1000");
await expect(inputView.purchaseAmountInput()).rejects.toThrow("[ERROR] ");
});

test("숫자가 아니면 예외가 발생한다", async () => {
Console.readLineAsync.mockResolvedValue("abc");
await expect(inputView.purchaseAmountInput()).rejects.toThrow("[ERROR]");
});
});

describe("winningNumbersInput", () => {
test("정상적인 당첨 번호 입력 시 배열을 반환한다", async () => {
Console.readLineAsync.mockResolvedValue("1,2,3,4,5,6");
const numbers = await inputView.winningNumbersInput();
expect(numbers).toEqual([1, 2, 3, 4, 5, 6]);
});
test("6개가 아니면 예외가 발생한다", async () => {
Console.readLineAsync.mockResolvedValue("1,2,3,4,5");
await expect(inputView.winningNumbersInput()).rejects.toThrow(
"[ERROR] 당첨 번호는 6개여야 합니다."
);
});
test("중복된 번호가 있으면 예외가 발생한다", async () => {
Console.readLineAsync.mockResolvedValue("1,2,3,4,5,5");
await expect(inputView.winningNumbersInput()).rejects.toThrow(
"[ERROR] 당첨 번호는 중복될 수 없습니다."
);
});
});
});
30 changes: 30 additions & 0 deletions __tests__/LottoMaker.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import LottoMaker from "../src/LottoMaker";

describe("LottoMaker 클래스 테스트", () => {
test("생성된 로또 번호는 1~45 범위 내의 숫자다", () => {
const lotto = LottoMaker.createLottoNumbers();
const numbers = lotto.getNumbers();

numbers.forEach((num) => {
expect(num).toBeGreaterThanOrEqual(1);
expect(num).toBeLessThanOrEqual(45);
});
});

test("생성된 로또 번호는 중복이 없다", () => {
const lotto = LottoMaker.createLottoNumbers();
const numbers = lotto.getNumbers();
const uniqueNumbers = new Set(numbers);

expect(uniqueNumbers.size).toBe(6);
});

test("생성된 로또 번호는 오름차순으로 정렬되어 있다", () => {
const lotto = LottoMaker.createLottoNumbers();
const numbers = lotto.getNumbers();

for (let i = 0; i < numbers.length - 1; i++) {
expect(numbers[i]).toBeLessThan(numbers[i + 1]);
}
});
});
6 changes: 6 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});

test("로또 번호가 6개 보다 작으면 예외가 발생한다", () => {
expect(() => {
new Lotto([1, 2, 3]);
}).toThrow("[ERROR]");
});

// TODO: 테스트가 통과하도록 프로덕션 코드 구현
test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
Expand Down
29 changes: 29 additions & 0 deletions __tests__/WinningResult.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Lotto from "../src/Lotto";
import WinningResult from "../src/WinningResult";

describe("Winning Result 클래스 테스트", () => {
test("3개 일치 시 통계가 정확히 동작한다.", () => {
const result = new WinningResult();
const lotto = new Lotto([1, 2, 3, 10, 11, 12]);
const winningNumbers = [1, 2, 3, 4, 5, 6];
const bonusNumber = 7;

result.checkWinning(lotto, winningNumbers, bonusNumber);
const statistics = result.getStatistics();
expect(statistics["3개 일치"]).toBe(1);
expect(statistics["4개 일치"]).toBe(0);
});

test("총 상금을 정확히 계산한다.", () => {
const result = new WinningResult();
const lotto1 = new Lotto([1, 2, 3, 10, 11, 12]);
const lotto2 = new Lotto([1, 2, 3, 4, 11, 12]);
const winningNumbers = [1, 2, 3, 4, 5, 6];
const bonusNumber = 7;
result.checkWinning(lotto1, winningNumbers, bonusNumber);
result.checkWinning(lotto2, winningNumbers, bonusNumber);

const totalPrize = result.calculateTotalPrize();
expect(totalPrize).toBe(5000 + 50000);
});
});
41 changes: 40 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
import InputView from "./InputView.js";
import OutputView from "./OutputView.js";
import LottoMaker from "./LottoMaker.js";
import WinningResult from "./WinningResult.js";
import { Console } from "@woowacourse/mission-utils";

class App {
async run() {}
async run() {
try {
const inputView = new InputView();

const purchaseAmount = await inputView.purchaseAmountInput();
const count = Math.floor(purchaseAmount / 1000);

OutputView.printPurchaseResult(count);

const lottos = [];
for (let i = 0; i < count; i++) {
const lotto = LottoMaker.createLottoNumbers();
lottos.push(lotto);
OutputView.printLottoNumbers(lotto);
}

const winningNumbers = await inputView.winningNumbersInput();
const bonusNumber = await inputView.bonusNumberInput();

const result = new WinningResult();
for (const lotto of lottos) {
result.checkWinning(lotto, winningNumbers, bonusNumber);
}

const totalPrize = result.calculateTotalPrize();
OutputView.printStatistics(
result.getStatistics(),
totalPrize,
purchaseAmount
);
} catch (error) {
Console.print(error.message);
}
}
}

export default App;
53 changes: 53 additions & 0 deletions src/InputView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Console } from "@woowacourse/mission-utils";

class InputView {
async purchaseAmountInput() {
const input = await Console.readLineAsync("구입 금액을 입력해 주세요.\n");
const amount = Number(input);

if (isNaN(amount) || amount <= 0) {
throw new Error("[ERROR] 구입 금액은 양수여야 합니다.");
}

if (amount % 1000 !== 0) {
throw new Error("[ERROR] 구입 금액은 1,000원 단위여야 합니다.");
}

return amount;
}

async winningNumbersInput() {
const input = await Console.readLineAsync("\n당첨 번호를 입력해주세요.\n");
const numbers = input.split(",").map((num) => Number(num.trim()));

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

if (numbers.some((num) => isNaN(num) || num < 1 || num > 45)) {
throw new Error("[ERROR] 당첨 번호는 1부터 45 사이의 숫자여야 합니다.");
}

const uniqueNumbers = new Set(numbers);
if (uniqueNumbers.size !== 6) {
throw new Error("[ERROR] 당첨 번호는 중복될 수 없습니다.");
}

return numbers;
}

async bonusNumberInput() {
const input = await Console.readLineAsync(
"\n보너스 번호를 입력해주세요.\n"
);
const number = Number(input.trim());

if (isNaN(number) || number < 1 || number > 45) {
throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.");
}

return number;
}
}

export default InputView;
22 changes: 21 additions & 1 deletion src/Lotto.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,29 @@ class Lotto {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}

const uniqueNumbers = new Set(numbers);
if (uniqueNumbers.size !== 6) {
throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다.");
}

const isInvalidRange = numbers.some((num) => num < 1 || num > 45);
if (isInvalidRange) {
throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
}

// TODO: 추가 기능 구현
getNumbers() {
return [...this.#numbers];
}

countMatches(winningNumbers) {
return this.#numbers.filter((num) => winningNumbers.includes(num)).length;
}

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

export default Lotto;
13 changes: 13 additions & 0 deletions src/LottoMaker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Random } from "@woowacourse/mission-utils";
import Lotto from "./Lotto.js";

class LottoMaker {
static createLottoNumbers() {
const numbers = Random.pickUniqueNumbersInRange(1, 45, 6).sort(
(a, b) => a - b
);
return new Lotto(numbers);
}
}

export default LottoMaker;
30 changes: 30 additions & 0 deletions src/OutputView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Console } from "@woowacourse/mission-utils";
import WinningResult from "./WinningResult.js";

class OutputView {
static printPurchaseResult(count) {
Console.print(`\n${count}개를 구매했습니다.`);
}

static printLottoNumbers(lotto) {
Console.print(`[${lotto.getNumbers().join(", ")}]`);
}

static printStatistics(statistics, totalPrize, purchaseAmount) {
Console.print("\n당첨 통계");
Console.print("---");

for (const [rank, prize] of Object.entries(WinningResult.PRIZE)) {
const label = prize.label;
const count = statistics[label];
Console.print(
`${label} (${prize.prize.toLocaleString()}원) - ${count}개`
);
}

const rateOfReturn = ((totalPrize / purchaseAmount) * 100).toFixed(1);
Console.print(`총 수익률은 ${rateOfReturn}%입니다.`);
}
}

export default OutputView;
Loading