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

[로또] 김도이 미션 제출합니다 #389

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
51ae201
docs: README.md에 기능목록 작성
d0ikim Oct 31, 2024
0575702
feat: 로또 구입 금액 입력받는 기능 구현
d0ikim Oct 31, 2024
3d1bdd4
feat: 입력받은 금액 만큼 로또 발행하는 기능 구현
d0ikim Oct 31, 2024
440dc37
feat: 발행된 로또 숫자들 오름차순 정렬하는 기능 구현
d0ikim Oct 31, 2024
e6fd46a
feat: 발행한 로또 수량 안내 출력하는 기능 구현
d0ikim Oct 31, 2024
4cecb50
feat: 로또 번호들을 로또 수량만큼 출력하는 기능 구현
d0ikim Oct 31, 2024
468d759
feat: 유저가 고른 6개의 당첨번호 입력받는 기능 구현
d0ikim Oct 31, 2024
4108f5c
feat: 로또구입금액 입력값이 숫자가 아닌 경우 예외 처리
d0ikim Nov 1, 2024
ad3611e
refactor: 당첨번호 입력받는 기능을 메서드로 추출
d0ikim Nov 1, 2024
0e362c6
feat: 당첨번호를 구분자(,) 기준으로 분리해 배열에 저장하는 기능 구현
d0ikim Nov 1, 2024
d6bd8bc
feat: 분리된 문자열 당첨번호 배열을 숫자형으로 변환하는 기능 구현
d0ikim Nov 1, 2024
f252127
feat: 로또 클래스에 번호 생성 메서드 추가
d0ikim Nov 3, 2024
a1bc278
refactor: 로또 클래스의 번호 생성 메서드 호출로 App.js 코드 정리
d0ikim Nov 3, 2024
3fb0a0d
feat: 보너스번호 입력받는 기능 구현
d0ikim Nov 3, 2024
3c747ce
Feat: 로또 클래스에 점수 계산해 반환하는 메서드 구현
d0ikim Nov 3, 2024
c845797
feat: 로또 클래스에 당첨금액 계산해 등수와 함께 반환하는 메서드 구현
d0ikim Nov 3, 2024
a3f56a0
refactor: 잘못된 자료형이었던 로직 올바르게 수정
d0ikim Nov 4, 2024
3f6dfe3
feat: 로또 클래스의 당첨금액 계산해 반환하는 메서드명 수정
d0ikim Nov 4, 2024
e61ec7c
refactor: 로또 클래스의 점수 계산해 객체 반환하는 메서드 가독성 개선 및 정리
d0ikim Nov 4, 2024
1fbb247
feat: 당첨결과 계산하고 출력하는 메서드 구현
d0ikim Nov 4, 2024
0ed47bc
feat: 로또 당첨번호 및 보너스번호 검증 로직 추가
d0ikim 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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# javascript-lotto-precourse

# 기능 목록

## 입력

- [x] 1,000원 단위로 로또 구입 금액을 입력받는다.
- [ ] 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨다. 잘못된 값은 아래에 해당한다.
- [x] 1,000원으로 나누어 떨어지지 않는 경우 = 1,000원 미만인 경우
- [x] 입력값이 숫자가 아닌 경우
- [x] 에러 발생 후, 해당 지점부터 다시 입력받는다.
- [x] 로또발행 내역이 출력된 후, 쉼표(,)를 기준으로 6개의 당첨 번호를 입력받는다.
- [x] 예외처리 `Error` 예외의 경우는 밑에 해당한다.
- [x] 1~45 사이의 숫자가 아닌 경우
- [x] 중복된 번호가 있는 경우
- [x] 6개의 번호를 입력받지 않은 경우
- [x] 당첨번호 입력받은 후, 보너스 번호를 입력받는다.
- [x] 예외처리 `Error`
- [x] 1~45 사이의 숫자가 아닌 경우
- [x] 당첨번호 안에 이미 보너스 번호가 있는 경우

## 출력

- [x] 발행한 로또 수량을 출력한다.
- [x] 오름차순 정렬된 로또 번호들을 로또 수량만큼 출력한다.
- [x] 계산되어 반환된 당첨내역을 출력한다.
- [x] 계산되어 반환된 수익률을 출력한다.

## 핵심 기능

- [x] 입력받은 로또 구입 금액에 해당하는 만큼 로또를 발행한다. (로또 1장은 1,000원이다)
- [x] 로또 1장 당 1~45 사이의 중복되지 않는 6개의 숫자를 뽑는다.
- [x] 숫자들을 오름차순 정렬한다.
- [x] 입력받은 당첨 번호 문자열을 구분자(,)를 기준으로 배열에 저장한다.
- [x] 구분자로 분리된 당첨 번호 문자열 배열을 숫자형으로 변환한다.
- [ ] 사용자가 구매한 로또 번호와, 당첨 번호를 비교해 당첨내역을 계산해 반환한다.

- [x] 점수를 계산한다.

- [x] 1등: 6개 번호 일치(6점)
- [x] 2등: 5개 번호(5점) + 보너스 번호 일치(true)
- [x] 3등: 5개 번호 일치(5점)
- [x] 4등: 4개 번호 일치(4점)
- [x] 5등: 3개 번호 일치(3점)

- [x] 당첨금액을 계산해 반환한다.
- [x] 1등 : 2,000,000,000원
- [x] 2등 : 30,000,000원
- [x] 3등 : 1,500,000원
- [x] 4등 : 50,000원
- [x] 5등 : 5,000원
- [x] 소수점 둘째 자리에서 반올림 한 수익률을 계산해 반환한다.
19 changes: 13 additions & 6 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});

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

test("로또 번호에 중복된 숫자가 없다면 false를 리턴한다.", () => {
expect(Lotto.hasDuplicatedLottoNumber([1, 2, 3, 4, 5, 6])).toBe(false);
});

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
test("번호에 1과 45 사이의 값이 아니면 true를 반환한다.", () => {
expect(Lotto.isValidLottoNumberRange([46])).toBe(false);
});

test("번호에 1과 45 사이의 값이 있으면 false를 반환한다.", () => {
expect(Lotto.isValidLottoNumberRange([4])).toBe(true);
});
});
141 changes: 140 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,144 @@
import { MissionUtils } from "@woowacourse/mission-utils";
import Lotto from "./Lotto.js";

const LOTTO_COST_MIN = 1000;
const GUIDE_MESSAGE_INPUT_COST = "구입금액을 입력해 주세요. (ex: 2000)\n";
const GUIDE_MESSAGE_INPUT_USER_PICKED_NUMBERS =
"\n당첨 번호를 입력해 주세요. (ex: 1,2,3,4,5,6)\n";
const GUIDE_MESSAGE_INPUT_USER_PICKED_BONUS_NUMBER =
"\n보너스 번호를 입력해 주세요. (ex: 7)\n";
const ERROR_MESSAGE_INVALID_COST =
"[ERROR] 입력하신 금액이 1,000원 단위가 아닙니다. 다시";
const ERROR_MESSAGE_NOT_NUMBER = "[ERROR] 입력값이 숫자가 아닙니다. 다시";
const ERROR_MESSAGE_NOT_IN_VALID_RANGE =
"[ERROR] 입력값이 로또 번호 범위에 있지 않습니다. 다시";
const ERROR_MESSAGE_DUPLICATED_LOTTO_NUMBER =
"[ERROR] 중복된 로또 번호가 있습니다. 다시";
const ERROR_MESSAGE_NOT_SIX_NUMBERS =
"[ERROR] 6개의 당첨 번호를 입력해야합니다. 다시";

class App {
async run() {}
async run() {
let lottoBudget;
let isValid = false;

while (!isValid) {
try {
lottoBudget = Number(await this.getLottoCost());
if (isNaN(lottoBudget)) {
throw new Error(ERROR_MESSAGE_NOT_NUMBER);
} else if (lottoBudget % LOTTO_COST_MIN !== 0) {
throw new Error(ERROR_MESSAGE_INVALID_COST);
}
isValid = true;
} catch (error) {
MissionUtils.Console.print(error.message);
}
}

const lottoCnt = lottoBudget / LOTTO_COST_MIN;
let lottoNumbers = [];
for (let i = 0; i < lottoCnt; i++) {
const randomNumbers = Lotto.generateRandomNumbers();

lottoNumbers.push(new Lotto(randomNumbers));
}

MissionUtils.Console.print(`\n${lottoCnt}개를 구매했습니다.`);

lottoNumbers.forEach((eachLotto) => {
MissionUtils.Console.print(`[${eachLotto.getNumbers().join(", ")}]`);
});

let userPickedNumbers = [];

isValid = false;
while (!isValid) {
try {
userPickedNumbers = this.splitUserPickedNumbers(
await this.getUserPickedNumbers()
).map(Number);

if (userPickedNumbers.length != 6) {
throw new Error(ERROR_MESSAGE_NOT_SIX_NUMBERS);
}

userPickedNumbers.forEach((number) => {
if (!Lotto.isValidLottoNumberRange(number)) {
throw new Error(ERROR_MESSAGE_NOT_IN_VALID_RANGE);
}
});

if (Lotto.hasDuplicatedLottoNumber(userPickedNumbers)) {
throw new Error(ERROR_MESSAGE_DUPLICATED_LOTTO_NUMBER);
}

isValid = true;
} catch (error) {
MissionUtils.Console.print(error.message);
}
}

let userPickedBonusNum;

isValid = false;
while (!isValid) {
try {
userPickedBonusNum = Number(await this.getUserPickedBonusNumber());

if (userPickedNumbers.includes(userPickedBonusNum)) {
throw new Error(ERROR_MESSAGE_DUPLICATED_LOTTO_NUMBER);
}

if (!Lotto.isValidLottoNumberRange(userPickedBonusNum)) {
throw new Error(ERROR_MESSAGE_NOT_IN_VALID_RANGE);
}

isValid = true;
} catch (error) {
MissionUtils.Console.print(error.message);
}
}

let score = Lotto.getScore(
userPickedNumbers,
lottoNumbers,
userPickedBonusNum
);

let counts = Lotto.getCounts(score);

// 당첨 내역 반환된거 출력
let resultMessage = Lotto.getResultMessage(...counts);
MissionUtils.Console.print(`${resultMessage}`);

let totalPrizeMoney = Lotto.calculateTotalPrizeMoney(counts);

const profitRatio = Lotto.getProfitRatio(lottoBudget, totalPrizeMoney);
// 수익률 반환된거 출력
MissionUtils.Console.print(`총 수익률은 ${profitRatio}%입니다.`);
}

getLottoCost() {
return MissionUtils.Console.readLineAsync(GUIDE_MESSAGE_INPUT_COST);
}

getUserPickedNumbers() {
return MissionUtils.Console.readLineAsync(
GUIDE_MESSAGE_INPUT_USER_PICKED_NUMBERS
);
}

splitUserPickedNumbers(userPickedStr) {
return userPickedStr.split(",");
}

getUserPickedBonusNumber() {
return MissionUtils.Console.readLineAsync(
GUIDE_MESSAGE_INPUT_USER_PICKED_BONUS_NUMBER
);
j;
}
}

export default App;
114 changes: 113 additions & 1 deletion src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import { MissionUtils } from "@woowacourse/mission-utils";
const LOTTO_NUMBER_MIN = 1;
const LOTTO_NUMBER_MAX = 45;
const LOTTO_NUMBER_COUNT = 6;

const FIRST_PRIZE_MONEY = 2000000000;
const SECOND_PRIZE_MONEY = 30000000;
const THIRD_PRIZE_MONEY = 1500000;
const FOURTH_PRIZE_MONEY = 50000;
const FIFTH_PRIZE_MONEY = 5000;

class Lotto {
#numbers;

Expand All @@ -12,7 +23,108 @@ class Lotto {
}
}

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

static generateRandomNumbers() {
return MissionUtils.Random.pickUniqueNumbersInRange(
LOTTO_NUMBER_MIN,
LOTTO_NUMBER_MAX,
LOTTO_NUMBER_COUNT
).sort((a, b) => a - b);
}

static getScore(userPickedNumbers, lottoNumbers, userPickedBonusNum) {
let scores = [];
lottoNumbers.forEach((lottoSet) => {
const winNum = lottoSet.getNumbers(); // 로또세트 순서대로 하나 가져오기
let score = 0; // 각 로또세트 별 점수 세고 계속 초기화되는 i 이터레이터
let isBonusMatched = 0;

// 사용자 번호와 로또 번호 비교
userPickedNumbers.forEach((userNumber) => {
if (winNum.includes(userNumber)) {
score++;
}
});

// 보너스 번호 확인
if (winNum.includes(userPickedBonusNum)) {
isBonusMatched = 1;
}

// 점수 객체를 scores 배열에 추가
scores.push({ score, isBonusMatched });
});

return scores; // {점수, 보너스여부(1)} 객체가 한 칸씩 들어가있는 배열 반환
}

static getCounts(scoreObj) {
let count3 = 0,
count4 = 0,
count5 = 0,
countBonus = 0,
count6 = 0;

scoreObj.forEach(({ score, isBonusMatched }) => {
if (score === 6) count6++;
else if (score === 5 && isBonusMatched) countBonus++;
else if (score === 5) count5++;
else if (score === 4) count4++;
else if (score === 3) count3++;
});

return [count3, count4, count5, countBonus, count6];
}

static getResultMessage(count3, count4, count5, countB, count6) {
const RESULT_MESSAGE = `당첨 통계\n---\n
3개 일치 (5,000원) - ${count3}개\n
4개 일치 (50,000원) - ${count4}개\n
5개 일치 (1,500,000원) - ${count5}개\n
5개 일치, 보너스 볼 일치 (30,000,000원) - ${countB}개\n
6개 일치 (2,000,000,000원) - ${count6}개\n`;
return RESULT_MESSAGE;
}

static calculateTotalPrizeMoney(countArr) {
let totalPrizeMoney = 0;
for (let i = 0; i < countArr.length; i++) {
if (i === 0) {
totalPrizeMoney += FIFTH_PRIZE_MONEY * countArr[i];
} else if (i === 1) {
totalPrizeMoney += FOURTH_PRIZE_MONEY * countArr[i];
} else if (i === 2) {
totalPrizeMoney += THIRD_PRIZE_MONEY * countArr[i];
} else if (i === 3) {
totalPrizeMoney += SECOND_PRIZE_MONEY * countArr[i];
} else if (i === 4) {
totalPrizeMoney += FIRST_PRIZE_MONEY * countArr[i];
}
}

return totalPrizeMoney;
}

static getProfitRatio(lottoCost, prize) {
let ratio = prize / lottoCost;
ratio = Math.trunc(ratio * 10000);
ratio = Math.round(ratio);
ratio = ratio / 100;

return ratio;
}

static isValidLottoNumberRange(number) {
return number >= LOTTO_NUMBER_MIN && number <= LOTTO_NUMBER_MAX;
}

static hasDuplicatedLottoNumber(numbers) {
const numberSet = new Set(numbers);
return numberSet.size !== numbers.length;
}
}

export default Lotto;