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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# javascript-lotto-precourse
1. 로또 구입 금액을 입력받는다. (Console API 사용)
2. 구입 금액이 1,000원 단위인지 검증한다. (예외 처리)
3. 당첨 번호 6개를 입력받는다. (쉼표 구분)
4. 당첨 번호의 유효성을 검증한다. (1~45 범위, 중복, 개수 등)
5. 보너스 번호 1개를 입력받는다.
6. 보너스 번호의 유효성을 검증한다. (1~45 범위, 당첨 번호와 중복 등)
7. 구입 금액에 맞춰 로또를 발행한다. (1장=1000원)
8. 로또 번호는 Random.pickUniqueNumbersInRange를 사용하여 6개를 뽑는다.
9. 발행된 로또 번호를 오름차순으로 정렬하여 출력한다.
10. 구매 로또별로 당첨 번호와 비교하여 일치 개수 및 보너스 번호 일치 여부를 계산한다.
11. 계산 결과에 따라 5등부터 1등까지 당첨 내역을 집계한다.
12. 당첨 내역(매칭 개수) 및 금액을 출력 요구사항 형식에 맞춰 출력한다.
13. 총 상금과 구입 금액을 이용하여 수익률을 계산한다.
14. 수익률을 소수점 둘째 자리에서 반올림하여 출력한다. (예: 62.5%)
35 changes: 33 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import { Console } from "@woowacourse/mission-utils";
import LottoGame from "./LottoGame.js";

class App {
async run() {}
async run() {
const game = new LottoGame();

try {
// 1. 구입 금액 입력 및 로또 발행
await game.getPurchaseAmount();
game.issueLottos();

// 2. 당첨 번호 입력 및 검증 (LottoGame에 구현된 반복 입력 처리)
const winningLottoInstance = await game.getWinningNumbers();
const winningNumbers = winningLottoInstance.getNumbers();

// 3. 보너스 번호 입력 및 검증 (LottoGame에 구현된 반복 입력 처리)
// LotteGame에 추가한 헬퍼 메서드를 통해 유효한 번호를 받습니다.
const bonusNumber = await game.getBonusNumberValue();

// 4. WinningLotto 객체 생성 (최종 중복 검사)
// LottoGame.js의 calculateAndPrintResults에서 WinningLotto 객체를 생성합니다.

// 5. 당첨 결과 계산 및 출력 (10~14번 기능)
game.calculateAndPrintResults(winningNumbers, bonusNumber);

} catch (error) {
// 1.6번 기능: [ERROR] 메시지 출력 후 애플리케이션 종료
Console.print(error.message);
// 테스트 통과 및 비정상 종료 상태를 명확히 하기 위해 throw error를 유지합니다.
throw error;
}
}
}

export default App;
export default App;
30 changes: 29 additions & 1 deletion src/Lotto.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,43 @@ class Lotto {
constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
this.#numbers.sort((a, b) => a - b); // 9. 번호 정렬 (미리 해둠)
}

// 기존 #validate에 유효성 검사를 추가합니다.
#validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}
// 4. 유효성 검증: 1~45 범위 및 중복 검사
if (numbers.some(number => number < 1 || number > 45)) {
throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
if (new Set(numbers).size !== 6) {
throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다.");
}
}

getNumbers() {
return this.#numbers;
}

// 10번 기능: 당첨 번호와의 일치 개수 및 보너스 번호 일치 여부 계산
getMatchResult(winningLotto, bonusNumber) {
const winningNumbers = winningLotto.getNumbers();

// 일치 개수 계산
const matchCount = this.#numbers.filter(number =>
winningNumbers.includes(number)
).length;

// 보너스 번호 일치 여부
const bonusMatch = this.#numbers.includes(bonusNumber);

return { matchCount, bonusMatch };
}

// TODO: 추가 기능 구현
// ... (나중에 추가할예정)
}

export default Lotto;
129 changes: 129 additions & 0 deletions src/LottoGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Console, Random } from "@woowacourse/mission-utils";
import Lotto from "./Lotto.js";
// PRIZES 상수를 사용하기 위해 WinningLotto에서 import해야 합니다.
import WinningLotto, { PRIZES } from "./WinningLotto.js";

const LOTTO_PRICE = 1000;

// 당첨 내역 초기화 및 출력 순서를 정의합니다. (파일 상단에 위치)
const INITIAL_STATS = {
[PRIZES.FIFTH.label]: 0,
[PRIZES.FOURTH.label]: 0,
[PRIZES.THIRD.label]: 0,
[PRIZES.SECOND.label]: 0,
[PRIZES.FIRST.label]: 0,
};

// ✨ 클래스는 파일당 한 번만 정의됩니다.
class LottoGame {
constructor() {
this.lottos = [];
this.purchaseAmount = 0;
}

// 1, 2번 기능: 구입 금액 입력 및 검증 로직 (반복 입력 처리 포함)
// 3, 4번 기능: 당첨 번호 입력 및 검증 (반복 처리)
// 5번 기능: 보너스 번호 입력 및 기본 유효성 검증 (반복 처리)
async getBonusNumberValue() {
while (true) {
try {
const input = await Console.readLineAsync("\n보너스 번호를 입력해 주세요.\n");
const number = parseInt(input.trim(), 10);

// 기본 유효성 검증 (숫자형, 1~45 범위)
if (isNaN(number) || number < 1 || number > 45) {
// WinningLotto.js의 에러 메시지를 재사용하거나 적절한 에러를 던집니다.
throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
return number;
} catch (error) {
Console.print(error.message);
}
}
}

// 5, 6번 기능: 보너스 번호 입력 및 검증 (반복 처리)
async getBonusNumber(winningLotto) {
while (true) {
try {
const input = await Console.readLineAsync("\n보너스 번호를 입력해 주세요.\n");
const number = parseInt(input.trim(), 10);

// 6. 유효성 검증: WinningLotto 생성자를 통해 검증이 실행됨
return new WinningLotto(winningLotto.getNumbers(), number);
} catch (error) {
Console.print(error.message);
}
}
}

// 2번 기능: 금액 유효성 검증 로직
validateAmount(input) {
const amount = parseInt(input.trim(), 10);
if (isNaN(amount)) {
throw new Error("[ERROR] 구입 금액은 숫자여야 합니다.");
}
if (amount % LOTTO_PRICE !== 0 || amount === 0) {
throw new Error("[ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다.");
}
return amount;
}

// 7, 8, 9번 기능: 로또 발행 로직
issueLottos() {
const count = this.purchaseAmount / LOTTO_PRICE;
Console.print(`\n${count}개를 구매했습니다.`);

for (let i = 0; i < count; i++) {
const numbers = Random.pickUniqueNumbersInRange(1, 45, 6);
this.lottos.push(new Lotto(numbers));
Console.print(`[${this.lottos[i].getNumbers().join(', ')}]`);
}
}

// 12, 13번 기능: 당첨 통계 계산 및 출력
calculateAndPrintResults(winningNumbers, bonusNumber) {
// winningLotto 인스턴스 생성 시 유효성 검사가 WinningLotto 생성자에서 실행됩니다.
const winningLotto = new WinningLotto(winningNumbers, bonusNumber);

const stats = this.lottos.reduce((acc, lotto) => {
const rank = winningLotto.rank(lotto);
if (rank) {
acc[rank.label] += 1;
}
return acc;
}, { ...INITIAL_STATS });

this.printWinningStats(stats);
this.printProfitRate(stats);
}

// 12번 기능: 당첨 내역 출력
printWinningStats(stats) {
Console.print('\n당첨 통계');
Console.print('---');
Object.keys(INITIAL_STATS).forEach(label => {
Console.print(`${label} - ${stats[label]}개`);
});
}

// 14번 기능: 수익률 계산 및 출력
printProfitRate(stats) {
let totalRevenue = 0;
Object.keys(stats).forEach(label => {
// PRIZES 상수는 WinningLotto에서 export 했으므로 여기에서 직접 접근 가능
const prize = Object.values(PRIZES).find(p => p.label === label);
if (prize) { // prize가 존재하는지 확인 (안전성 추가)
totalRevenue += stats[label] * prize.amount;
}
});

const profitRate = (totalRevenue / this.purchaseAmount) * 100;

// 수익률 소수점 둘째 자리에서 반올림
const roundedRate = Math.round(profitRate * 10) / 10;
Console.print(`총 수익률은 ${roundedRate.toLocaleString('ko-KR')}%입니다.`);
}
}

export default LottoGame;
50 changes: 50 additions & 0 deletions src/WinningLotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Lotto from "./Lotto.js";

// 1. PRIZES 상수를 파일 맨 위에 정의하고 export 합니다.
export const PRIZES = {
FIRST: { count: 6, bonus: false, amount: 2000000000, label: '6개 일치 (2,000,000,000원)' },
SECOND: { count: 5, bonus: true, amount: 30000000, label: '5개 일치, 보너스 볼 일치 (30,000,000원)' },
THIRD: { count: 5, bonus: false, amount: 1500000, label: '5개 일치 (1,500,000원)' },
FOURTH: { count: 4, bonus: false, amount: 50000, label: '4개 일치 (50,000원)' },
FIFTH: { count: 3, bonus: false, amount: 5000, label: '3개 일치 (5,000원)' },
};

class WinningLotto {
constructor(winningNumbers, bonusNumber) {
this.winningLotto = new Lotto(winningNumbers);
this.bonusNumber = this.validateBonusNumber(bonusNumber);
}

// 6번 기능: 보너스 번호 유효성 검증 로직
validateBonusNumber(number) {
const num = parseInt(number, 10);

if (isNaN(num)) {
throw new Error("[ERROR] 보너스 번호는 숫자여야 합니다.");
}
if (num < 1 || num > 45) {
throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
if (this.winningLotto.getNumbers().includes(num)) {
throw new Error("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.");
}

return num;
}

// 11번 기능: 로또 번호와 비교하여 당첨 등수를 반환
rank(lotto) {
const { matchCount, bonusMatch } = lotto.getMatchResult(this.winningLotto, this.bonusNumber);

// 1등부터 5등까지 순차적으로 조건 검사
if (matchCount === 6) return PRIZES.FIRST;
if (matchCount === 5 && bonusMatch) return PRIZES.SECOND;
if (matchCount === 5) return PRIZES.THIRD;
if (matchCount === 4) return PRIZES.FOURTH;
if (matchCount === 3) return PRIZES.FIFTH;

return null; // 낙첨
}
}

export default WinningLotto;