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

[로또] 이미진 미션 제출합니다 #397

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import App from "../src/App.js";
import { MissionUtils } from "@woowacourse/mission-utils";

// 입력값을 미리 설정하기 위해 MissionUtils.Console.readLineAsync 를 모킹 ( 제공된 입력값들을 순차적으로 반환하도록 설정)
const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();

Expand All @@ -11,19 +12,21 @@ const mockQuestions = (inputs) => {
});
};

// 로또 번호를 미리 설정하기 위해 MissionUtils.Random.pickUniqueNumbersInRange 메서드를 모킹
const mockRandoms = (numbers) => {
MissionUtils.Random.pickUniqueNumbersInRange = jest.fn();
numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, MissionUtils.Random.pickUniqueNumbersInRange);
};

// MissionUtils.Console.print메서드를 감시하는 logSpy 객체를 만든다. 실제 출력된 로그를 확인하고 검증할 수 있다.
const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, "print");
logSpy.mockClear();
return logSpy;
};

// 특정 입력값을 주고 실행한 후 오류 메시지를 검증하는 함수
const runException = async (input) => {
// given
const logSpy = getLogSpy();
Expand Down
9 changes: 8 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import LottoGame from "./LottoGame.js";
// 프로그램의 진입점이자 전체 흐름을 관리하는 역할
// 비동기 작업을 수행하기 위한 메서드
// run() 메서드는 프로그램의 메인 진입점. 이 메서드에서 로또 게임의 각 단계를 비동기로 처리한다.
class App {
async run() {}
async run() {
const game = new LottoGame();
await game.start();
}
}

export default App;
13 changes: 13 additions & 0 deletions src/ConsoleHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MissionUtils } from "@woowacourse/mission-utils";
//사용자 입출력 처리 모듈
class ConsoleHandler {
static async readLineAsync(prompt) {
return await MissionUtils.Console.readLineAsync(prompt);
}

static print(message) {
MissionUtils.Console.print(message);
}
}

export default ConsoleHandler;
9 changes: 9 additions & 0 deletions src/Constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 상금, 로또 가격 등의 상수를 관리한다.
export const LOTTO_PRICE = 1000;
export const PRIZE_TABLE = {
1: 2000000000,
2: 30000000,
3: 1500000,
4: 50000,
5: 5000,
}
27 changes: 21 additions & 6 deletions src/Lotto.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
// Lotto 클래스를 사용하여 로또 번호를 관리하는 클래스
class Lotto {
#numbers;
#numbers; // 프라이빗 필드로 numbers 선언

constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
this.#validate(numbers); // 유효성 검사
this.#numbers = numbers; // 유효한 경우에만 필드에 할당
}

// numbers 배열이 유효한지 확인하는 메서드
#validate(numbers) {
if (!Array.isArray(numbers)) {
throw new Error("[ERROR] 로또 번호는 배열이어야 합니다.");
}
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
throw new Error("[ERROR] 로또 번호는 6개의 숫자여야 합니다.");
}
if (!numbers.every(num => typeof num === 'number' && num >= 1 && num <= 45)) {
throw new Error("[ERROR] 로또 번호는 1 ~ 45 사이의 숫자여야 합니다.");
}
if (new Set(numbers).size !== numbers.length) {
throw new Error("[ERROR] 로또 번호는 중복되지 않는 숫자여야 합니다.");
}
}

// TODO: 추가 기능 구현

// numbers 필드에 저장된 로또 번호를 반환하는 메서드
getNumbers() {
return [...this.#numbers]; // numbers 배열의 복사본을 반환하여 외부에서의 변경을 방지
}
}

// Lotto 클래스를 외부에서 사용할 수 있도록 export
export default Lotto;
117 changes: 117 additions & 0 deletions src/LottoGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { MissionUtils } from "@woowacourse/mission-utils";
import Lotto from "./Lotto.js";
import LottoResult from "./LottoResult.js";
import LottoMachine from "./LottoMachine.js"
import { LOTTO_PRICE } from "./Constants.js";

// 로또 게임의 메인 흐름을 관리
class LottoGame {
// 로또 게임의 초기화 작업
constructor(){
this.lottoMachine = new LottoMachine(); //로또 번호 생성기(인스턴스 생성)
this.lottoResult = new LottoResult(); //결과 계산기
}
// 비동기 작업을 수행하기 위한 메서드
async start(){
try
{
const amount = await this.inputPurchaseAmount(); //구입 금액 입력 및 검증

// 로또 발행
const purchasedLottos = this.purchaseLottos(amount);

this.printPurchasedLottos(purchasedLottos);

// 당첨 번호 및 보너스 번호 입력
const { winningNumbers, bonusNumber } = await this.inputWinningNumbers();

// 당첨 결과 계산
this.lottoResult.calculateRank(purchasedLottos, winningNumbers, bonusNumber);

// 당첨 결과 및 통계 출력
this.printLottoStatistics(this.lottoResult.getStatistics(), amount);

} catch (error)
{
MissionUtils.Console.print(error.message);
}
}
//구입 금액 입려받고 유효성 검사.
async inputPurchaseAmount(){
const amountStr = await MissionUtils.Console.readLineAsync("구입 금액을 입력해 주세요: ");
const amount = parseInt(amountStr, 10); //문자열을 정수로 변환

if(isNaN(amount) || amount % LOTTO_PRICE !== 0) {
throw new Error("[ERROR] 로또 구입 금액은 1000원 단위여야 합니다.");
}
return amount;
}

purchaseLottos(amount){ //구매한 로또 배열 반환
const lottoCount = amount / LOTTO_PRICE;

const lottos = []; // 스택에 저장

for(let i = 0; i < lottoCount; i++){
const numbers = this.lottoMachine.generateLotto(); // 로또 번호 생성
lottos.push(numbers);
}

return lottos;
}

//당첨 번호 입력받기
async inputWinningNumbers() {
//사용자 입력 변수 선언
const WinningNumbersStr = await MissionUtils.Console.readLineAsync("당첨 번호를 입력해 주세요. ");
const bonusNumberStr = await MissionUtils.Console.readLineAsync("보너스 번호를 입력해 주세요. ");

const winningNumbers = WinningNumbersStr.split(",").map(Number); // , 단위로 나눈 뒤 매핑
const bonusNumber = parseInt(bonusNumberStr, 10); //정수로만 바꾸기

this.validateWinningNumbers(winningNumbers, bonusNumber); // 유효성 검사

return { winningNumbers, bonusNumber };
}

// 순회하면 유효성 검사
validateWinningNumbers(winningNumbers, bonusNumber){
if (winningNumbers.length !== 6 || new Set(winningNumbers).size !== 6){
throw new Error("[ERROR] 당첨 번호는 1부터 45 사이의 중복되지 않는 숫자 6개여야 합니다.");
}
if (!this.isValidLottoNumber(bonusNumber)){
throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.");
}
winningNumbers.forEach(num =>{
if(!this.isValidLottoNumber(num)){
throw new Error("[ERROR] 당첨 번호는 1부터 45 사이의 숫자여야 합니다. ");
}
});
}

isValidLottoNumber(num) {
return num >= 1 && num <= 45;
}

printPurchasedLottos(lottos) {
MissionUtils.Console.print(`${lottos.length}개를 구매했습니다.`);
lottos.forEach(lotto => {

MissionUtils.Console.print(`[${lotto.sort((a,b) => a - b).join(", ")}]`); // 오름차순으로 출력된다.
});
}

printLottoStatistics(statistics, purchaseAmount) {
MissionUtils.Console.print("당첨 통계");
MissionUtils.Console.print("-----------");

Object.entries(statistics.rankCounts).forEach(([rank,count]) => {
MissionUtils.Console.print(`${rank}등: ${count}개`);
});

const profitRate = this.lottoResult.calculateProfit(purchaseAmount);
MissionUtils.Console.print(`총 수익률은 ${profitRate}%입니다.`);
}
}

export default LottoGame;
18 changes: 18 additions & 0 deletions src/LottoMachine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MissionUtils } from "@woowacourse/mission-utils";
import Lotto from "./Lotto.js";
// 로또 티켓과 당첨 번호를 생성하는 역할

class LottoMachine {
generateLotto() {
const numbers = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6);

return numbers;
}

generateWinningNumbers(){
const winningNumbers = MissionUtils.Random.pickUniqueNumbersInRange(1,45,6);
const bonusNumber = MissionUtils.Random.pickUniqueNumbersInRange(1,45,1)[0];
return {winningNumbers, bonusNumber};
}
}
export default LottoMachine;
60 changes: 60 additions & 0 deletions src/LottoResult.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 당첨 여부를 계산하는 역할
import { PRIZE_TABLE } from "./Constants.js";

class LottoResult {
constructor() {
this.rankCounts = {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
};
this.totalProfit = 0;
}

calculateRank(purchasedLottos, winningNumbers, bonusNumber){
purchasedLottos.forEach(lotto => {
const matchCount = this.countMatchingNumbers(lotto, winningNumbers); //matchcount
const isBonusMatched = lotto.includes(bonusNumber); //isBonusMatched

const rank = this.getRank(matchCount, isBonusMatched);
if(rank) {
this.rankCounts[rank] += 1;
this.totalProfit += this.getPrize(rank);
}
});
}

countMatchingNumbers(lottoNumbers, winningNumbers) {
return lottoNumbers.filter(num => winningNumbers.includes(num)).length; //includes() 포함되어 있는 지 확인
} // 매칭된 수 반환

getRank(matchCount,isBonusMatched){
if(matchCount === 6) return 1;
if(matchCount === 5 && isBonusMatched) return 2;
if(matchCount === 5) return 3;
if(matchCount === 4) return 4;
if(matchCount === 3) return 5;
return null;
}

getPrize(rank) {
return PRIZE_TABLE[rank] || 0;
}

calculateProfit(purchaseAmount) {
const profitRate = (this.totalProfit / purchaseAmount) * 100;
return profitRate.toFixed(1); // 소수점 1자리까지 반올림
}

getStatistics(){
return {
rankCounts: this.rankCounts,
totalProfit: this.totalProfit,
};
}

}

export default LottoResult;