Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,72 @@
# javascript-lotto-precourse

간단한 로또 발매기를 구현한 프로젝트입니다.
사용자가 구입금액을 입력하면 로또를 생성하고, 당첨번호와 비교하여 당첨 내역과 수익률을 보여줍니다.

## 주요 기능

- 로또 자동 생성 및 출력
- 당첨번호 비교 및 상금 계산
- 수익률 계산 (소수점 둘째 자리 반올림)
- 예외 처리 및 재입력

## 프로젝트 구조

- **App**: 프로그램의 전체 흐름 관리
- **LottoManager**: 로또 생성 및 당첨금/수익률 계산
- **Lotto**: 개별 로또 번호 관리 및 검증
- **InputManager**: 사용자 입력받기 (숫자, 쉼표 구분 숫자)
- **Validator**: 입력값 유효성 검증
- **OutputManager**: 결과 출력
- **constants**: 상수 및 당첨금 정보 (Map 사용)

## 실행 방법

```bash
npm install
npm run start
```

## 테스트

```bash
npm run test
```

## 기능 목록

### 입력받기

- 1000원으로 나누어 떨어지는 금액을 입력받는다.
- [ERROR] 나누어 떨어지지 않으면 오류.
- [ERROR] 1000원 이상이 아니라면 오류.
- 쉼표 기준으로 구분되는 6개의 당첨 번호를 입력 받는다.
- [ERROR] 6개가 아니면 오류.
- [ERROR] 1부터 45까지의 숫자가 아니면 오류.
- [ERROR] 당첨 번호에 중복이 있으면 오류.
- 보너스 번호를 입력받는다.
- [ERROR] 1부터 45까지의 숫자가 아니면 오류.
- [ERROR] 당첨 번호와 보너스 번호가 중복되면 오류.

### 출력하기

- 구입한 로또 개수를 출력한다.
- "X개를 구매했습니다." 형식
- 생성한 로또를 출력한다.
- "[번호1, 번호2, ...]" 형식으로 오름차순 정렬
- 당첨 통계를 출력한다.
- 3개 일치부터 6개 일치까지 당첨 내역 출력
- 수익률을 소수점 둘째 자리에서 반올림하여 출력

### 로또 관리자

- 입력받은 구입금액에 맞춰 로또를 생성한다.
- 당첨 번호와 생성한 로또를 비교하여 당첨액을 계산한다.
- 당첨액에서 구입금액을 나눠서 수익률을 계산한다.

### 로또

- 로또 번호의 숫자 범위는 1부터 45까지.
- 로또 번호는 중복되지 않아야 한다.
- 로또 번호는 6개이다.
- 로또 번호는 오름차순으로 정렬한다.
91 changes: 91 additions & 0 deletions __tests__/LottoManagerTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { LottoManager } from "../src/LottoManager.js";
import { MissionUtils } from "@woowacourse/mission-utils";

describe("LottoManager 클래스 테스트", () => {
beforeEach(() => {
jest.spyOn(MissionUtils.Console, "print").mockImplementation();
});

afterEach(() => {
jest.restoreAllMocks();
});

test("구입금액이 1000원 미만이면 예외가 발생한다.", () => {
expect(() => {
new LottoManager(500);
}).toThrow("[ERROR]");
});

test("구입금액이 1000원으로 나누어떨어지지 않으면 예외가 발생한다.", () => {
expect(() => {
new LottoManager(1500);
}).toThrow("[ERROR]");
});

test("정상적인 구입금액으로 로또를 생성할 수 있다.", () => {
const lottoManager = new LottoManager(3000);
expect(lottoManager.getLottos().length).toBe(3);
});

test("구입한 로또 개수는 구입금액을 1000으로 나눈 값이다.", () => {
const lottoManager = new LottoManager(5000);
expect(lottoManager.getLottos().length).toBe(5);
});
});

describe("당첨금 계산 테스트", () => {
beforeEach(() => {
jest.spyOn(MissionUtils.Console, "print").mockImplementation();
jest.spyOn(MissionUtils.Random, "pickUniqueNumbersInRange").mockReturnValue([
1, 2, 3, 4, 5, 6,
]);
});

afterEach(() => {
jest.restoreAllMocks();
});

test("당첨이 없으면 상금이 0이다.", () => {
const lottoManager = new LottoManager(1000);
lottoManager.calculateWinningStatistics(
[40, 41, 42, 43, 44, 45],
39
);
expect(lottoManager.getPrizeAmount()).toBe(0);
});

test("6개 일치시 최고 상금을 받는다.", () => {
const lottoManager = new LottoManager(1000);
lottoManager.calculateWinningStatistics([1, 2, 3, 4, 5, 6], 7);
expect(lottoManager.getPrizeAmount()).toBe(2000000000);
});
});

describe("수익률 계산 테스트", () => {
beforeEach(() => {
jest.spyOn(MissionUtils.Console, "print").mockImplementation();
jest.spyOn(MissionUtils.Random, "pickUniqueNumbersInRange").mockReturnValue([
1, 2, 3, 4, 5, 6,
]);
});

afterEach(() => {
jest.restoreAllMocks();
});

test("당첨이 없으면 수익률은 0이다.", () => {
const lottoManager = new LottoManager(1000);
lottoManager.calculateWinningStatistics(
[40, 41, 42, 43, 44, 45],
39
);
expect(lottoManager.getROI()).toBe(0);
});

test("6개 일치시 수익률을 계산한다.", () => {
const lottoManager = new LottoManager(1000);
lottoManager.calculateWinningStatistics([1, 2, 3, 4, 5, 6], 7);
const roi = lottoManager.getROI();
expect(roi).toBe(200000000);
});
});
42 changes: 40 additions & 2 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,50 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});

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

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
test("로또 번호의 개수가 6개보다 적으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5]);
}).toThrow("[ERROR]");
});

test("정상적인 로또 번호를 생성할 수 있다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5, 6]);
});

test("6개 모두 일치하면 6을 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const matchCount = lotto.getMatchCount([1, 2, 3, 4, 5, 6], 7);
expect(matchCount).toBe(6);
});

test("5개가 일치하고 보너스가 일치하면 '5+bonus'를 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const matchCount = lotto.getMatchCount([1, 2, 3, 4, 5, 7], 6);
expect(matchCount).toBe("5+bonus");
});

test("5개가 일치하고 보너스가 일치하지 않으면 5를 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const matchCount = lotto.getMatchCount([1, 2, 3, 4, 5, 7], 8);
expect(matchCount).toBe(5);
});

test("3개가 일치하면 3을 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const matchCount = lotto.getMatchCount([1, 2, 3, 7, 8, 9], 10);
expect(matchCount).toBe(3);
});

test("일치하는 번호가 없으면 0을 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const matchCount = lotto.getMatchCount([7, 8, 9, 10, 11, 12], 13);
expect(matchCount).toBe(0);
});
});
24 changes: 23 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import { InputManager } from "./InputManager.js";
import { LottoManager } from "./LottoManager.js";
import { OutputManager } from "./OutputManager.js";
import { Validator } from "./Validator.js";

class App {
async run() {}
async run() {
try {
const purchaseAmount = await InputManager.readInputNumber(
`구입금액을 입력해 주세요.\n`
);
const lottoManager = new LottoManager(purchaseAmount);
const winningNumbers = await InputManager.readInputNumberWithComma(
`\n당첨 번호를 입력해 주세요.\n`
);
const bonusNumber = await InputManager.readInputNumber(
`\n보너스 번호를 입력해 주세요.\n`
);
Validator.validateWinningAndBonusNumbers(winningNumbers, bonusNumber);
lottoManager.calculateWinningStatistics(winningNumbers, bonusNumber);
} catch (error) {
OutputManager.print(`${error.message}`);
}
}
}

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

export class InputManager {
static async readInputNumber(msg) {
const inputStr = await Console.readLineAsync(`${msg}`);
Validator.validateNumber(inputStr);
return Number(inputStr.trim());
}

static async readInputNumberWithComma(msg) {
const input = await Console.readLineAsync(`${msg}`);
input.split(",").forEach((numStr) => Validator.validateNumber(numStr));
return input.split(",").map((numStr) => Number(numStr.trim()));
}
}
17 changes: 17 additions & 0 deletions src/Lotto.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,26 @@ class Lotto {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}
if (new Set([...numbers]).size !== 6) {
throw new Error("[ERROR] 로또 번호에 중복이 있습니다.");
}
}

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

getMatchCount(winningNumbers, bonusNumber) {
let matchCount = 0;
for (let lottoNum of this.#numbers) {
if (winningNumbers.includes(lottoNum)) matchCount++;
}
if (matchCount === 5 && this.#numbers.includes(bonusNumber)) {
return `${matchCount}+bonus`;
}
return matchCount;
}
}

export default Lotto;
85 changes: 85 additions & 0 deletions src/LottoManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Random } from "@woowacourse/mission-utils";
import {
COST_PER_LOTTO,
MIN_WINNING_MATCH,
PRIZE_BY_MATCH_COUNT,
} from "./constants.js";
import { OutputManager } from "./OutputManager.js";
import { Validator } from "./Validator.js";
import Lotto from "./Lotto.js";
import { formatCurrency } from "./utils.js";

export class LottoManager {
#lottoAmount;
#lottoCount;
#lottos = [];
#prizeCount = {};
#prizeAmount = 0;

constructor(purchaseAmount) {
Validator.validatePurchaseAmount(purchaseAmount);
this.#lottoAmount = purchaseAmount;
this.#lottoCount = purchaseAmount / COST_PER_LOTTO;
OutputManager.print(`\n${this.#lottoCount}개를 구매했습니다.`);
this.#makeLotto();
}

#makeLotto() {
for (let i = 0; i < this.#lottoCount; i++) {
const lottoNumbers = [...Random.pickUniqueNumbersInRange(1, 45, 6)].sort(
(a, b) => a - b
);
const newLotto = new Lotto(lottoNumbers);
this.#lottos.push(newLotto);
OutputManager.print(`[${newLotto.getNumbers().join(", ")}]`);
}
}

calculateWinningStatistics(winningNumbers, bonusNumber) {
this.#calculatePrizeAmount(winningNumbers, bonusNumber);
this.#printStatistics();
}

#calculatePrizeAmount(winningNumbers, bonusNumber) {
for (let lotto of this.#lottos) {
const result = lotto.getMatchCount(winningNumbers, bonusNumber);
if (result < MIN_WINNING_MATCH) continue;
this.#prizeCount[result] = (this.#prizeCount[result] || 0) + 1;
this.#prizeAmount += PRIZE_BY_MATCH_COUNT.get(String(result)).prize;
}
}

#printStatistics() {
const ROI = parseFloat(
((this.#prizeAmount / this.#lottoAmount) * 100).toFixed(2)
);
OutputManager.print(`\n당첨 통계`);
OutputManager.print(`---`);
this.#printPrizeDetails();
OutputManager.print(`총 수익률은 ${ROI}%입니다.`);
}

#printPrizeDetails() {
for (let [matchCount, { prize, label }] of PRIZE_BY_MATCH_COUNT) {
OutputManager.print(
`${label} (${formatCurrency(prize)}원) - ${
this.#prizeCount[matchCount] || 0
}개`
);
}
}

getLottos() {
return this.#lottos;
}

getPrizeAmount() {
return this.#prizeAmount;
}

getROI() {
return parseFloat(
((this.#prizeAmount / this.#lottoAmount) * 100).toFixed(2)
);
}
}
7 changes: 7 additions & 0 deletions src/OutputManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Console } from "@woowacourse/mission-utils";

export class OutputManager {
static print(msg) {
Console.print(`${msg}`);
}
}
Loading