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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,48 @@
# javascript-planetlotto-precourse
## 최종 테스트 (행성 로또)

## 기능 요구 사항

로또 번호의 숫자 범위는 1~30까지이다.
1개의 로또를 발행할 때 중복되지 않는 5개의 숫자를 뽑는다.
당첨 번호 추첨 시 중복되지 않는 숫자 5개와 보너스 번호 1개를 뽑는다.
당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
1등: 5개 번호 일치 / 100,000,000원
2등: 4개 번호 + 보너스 번호 일치 / 10,000,000원
3등: 4개 번호 일치 / 1,500,000원
4등: 3개 번호 일치 + 보너스 번호 일치 / 500,000원
5등: 2개 번호 일치 + 보너스 번호 일치 / 5,000원
로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
로또 1장의 가격은 500원이다.
당첨 번호와 보너스 번호를 입력받는다.
사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역을 출력하고 로또 게임을 종료한다.
사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 해당 지점부터 다시 입력을 받는다.

## 도전 과제

기본 요구 사항을 모두 충족한 후, 아래 중 하나를 선택하여 도전하세요. 단, 도전 과제 수행 여부와 관계없이 기본 기능은 반드시 작동해야 합니다.

## 도전 방향 (최대한 4시 59분 끝까지 도전)

## 1. 프리코스 기간 최종테스트 준비 도중에 연습했던 도전을 planetlotto로 추가 기능 구현 시도

(주제: 만약 이 로또 문제에 csv가 주어져서 문제가 나온다면? + 오픈 미션때 연습했던것처럼 실생활 같은 문제로 연결된다면?)

복권 판매점에서 복권을 판매하고 당첨 확인 및 수익률 통계를 관리하는 시스템을 만들어야 한다고 생각해보자.
그럴 경우 기능을 확장시켜야한다. 이 아이디어는 7기 최종 코딩 테스트를 문제를 프리코스 기간중에 풀어보고 직접 만들어 본 케이스이다.

csv 를 받아야 하므로 LotteryShop는 import fs from "fs"; 로 시작한다. 마찬가지로 로또는 const numbers = Random pickUniqueNumbersInRange(1, 30, 5); 로 생산한다고 가정한다.

csv는 간단하게 3개정도만 쉬는시간에 생각해본다.
type,name,price,firstPrize,secondPrize,thirdPrize,stock
LOTTO,로또6/45,1000,2000000000,30000000,1500000,100
PENSION,연금복권,1000,700000000,100000000,10000000,80
INSTANT,즉석복권,2000,10000000,1000000,100000,150

(App이랑 연결은 온라인 test 를 고려하여 로컬 npm test로 통과 되는지만 확인 후, 연결 중단)

---

2. 최대한 여러 검증을 추가하는 방향으로 진행을 할것.
3. 매직 넘버를 최대한 줄여본다.
4. 시간이 될 경우. 로또 이외에도 연금복권이랑 즉석복권까지 만들어본다.
151 changes: 150 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,154 @@
import { Random } from "@woowacourse/mission-utils";
import { InputView, OutputView } from "./view.js";
import Lotto from "./Lotto.js";
import { LOTTO_CONFIG, ERROR_MESSAGE } from "./constants.js";

class App {
async run() {}
async run() {
const amount = await this.#askAmount();
const lottoLists = this.#generateLottos(amount);
OutputView.printPurchasedLottos(lottoLists.length);
lottoLists.forEach((lotto) => OutputView.printLottos(lotto.numberCheck()));

const winningNumbers = await this.#askWinningLotto();
const bonusNumber = await this.#askBonusNumber(winningNumbers);

const result = this.#resultCalculate(
lottoLists,
winningNumbers,
bonusNumber
);
OutputView.printResult(result);
}

async #askAmount() {
try {
const input = await InputView.askAmount();
const amount = Number(input);
this.#validateAmount(amount);
return amount;
} catch (error) {
OutputView.printErrorMessage(error.message);
return this.#askAmount();
}
}

#validateAmount(amount) {
if (Number.isNaN(amount)) {
throw new Error(ERROR_MESSAGE.INVALID_NUMBER_FORMAT);
}
if (amount < LOTTO_CONFIG.PRICE) {
throw new Error(`[ERROR] ${LOTTO_CONFIG.PRICE}원 이상 입력해주세요.`);
}
if (amount % LOTTO_CONFIG.PRICE !== 0) {
throw new Error(`[ERROR] ${LOTTO_CONFIG.PRICE}원 단위로 입력해주세요.`);
}
}

async #askWinningLotto() {
try {
const input = await InputView.askWinningLotto();
const numbers = input.replaceAll(" ", "").split(",").map(Number);
this.#winningNumbersValidate(numbers);
return numbers;
} catch (error) {
OutputView.printErrorMessage(error.message);
return this.#askWinningLotto();
}
}

#winningNumbersValidate(numbers) {
if (numbers.some(Number.isNaN)) {
throw new Error(ERROR_MESSAGE.INVALID_NUMBER_FORMAT);
}
if (numbers.length !== LOTTO_CONFIG.NUMBER_COUNT) {
throw new Error(
`[ERROR] 로또 번호는 ${LOTTO_CONFIG.NUMBER_COUNT}개여야 합니다.`
);
}
if (new Set(numbers).size !== numbers.length) {
throw new Error(ERROR_MESSAGE.DUPLICATE_TARGET_NUMBER);
}
const 범위확인 = numbers.every(
(num) => num >= LOTTO_CONFIG.MIN_NUMBER && num <= LOTTO_CONFIG.MAX_NUMBER
);
if (!범위확인) {
throw new Error(
`[ERROR] 로또 번호는 ${LOTTO_CONFIG.MIN_NUMBER}부터 ${LOTTO_CONFIG.MAX_NUMBER} 사이의 숫자여야 합니다.`
);
}
}

async #askBonusNumber(winningNumbers) {
try {
const input = await InputView.askBonusNumber();
const number = Number(input);
this.#bonusNumberValidate(number, winningNumbers);
return number;
} catch (error) {
OutputView.printErrorMessage(error.message);
return this.#askBonusNumber(winningNumbers);
}
}

#bonusNumberValidate(number, winningNumbers) {
if (Number.isNaN(number)) {
throw new Error(ERROR_MESSAGE.INVALID_NUMBER_FORMAT);
}
if (number < LOTTO_CONFIG.MIN_NUMBER || number > LOTTO_CONFIG.MAX_NUMBER) {
throw new Error(
`[ERROR] 로또 번호는 ${LOTTO_CONFIG.MIN_NUMBER}부터 ${LOTTO_CONFIG.MAX_NUMBER} 사이의 숫자여야 합니다.`
);
}
if (winningNumbers.includes(number)) {
throw new Error(ERROR_MESSAGE.DUPLICATE_BONUS_NUMBER);
}
}

#generateLottos(amount) {
const count = amount / LOTTO_CONFIG.PRICE;
const lottoLists = [];

for (let i = 0; i < count; i++) {
const numbers = Random.pickUniqueNumbersInRange(
LOTTO_CONFIG.MIN_NUMBER,
LOTTO_CONFIG.MAX_NUMBER,
LOTTO_CONFIG.NUMBER_COUNT
);
lottoLists.push(new Lotto(numbers));
}

return lottoLists;
}

#rankCalculate(matchCount, hasBonus) {
if (matchCount === 5) return 1;
if (matchCount === 4 && hasBonus) return 2;
if (matchCount === 4) return 3;
if (matchCount === 3 && hasBonus) return 4;
if (matchCount === 2 && hasBonus) return 5;
return 0;
}

#resultCalculate(lottoLists, winningNumbers, bonusNumber) {
const result = new Map([
[0, 0],
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
]);

for (const lotto of lottoLists) {
const matchCount = lotto.matchCount(winningNumbers);
const hasBonus = lotto.hasBonus(bonusNumber);
const rank = this.#rankCalculate(matchCount, hasBonus);
result.set(rank, result.get(rank) + 1);
}

return result;
}
}

export default App;
110 changes: 110 additions & 0 deletions src/LotteryShop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import fs from "fs";
import { Random } from "@woowacourse/mission-utils";
import { LOTTERY_TYPES, PLUS_ERROR_MESSAGE } from "./constants.js";
import LottoLottery from "./LottoLottery.js";
import PensionLottery from "./PensionLottery.js";

class LotteryShop {
#lotteryTypes;
#purchaseHistory;
#salesStats;
constructor(csvPath) {
this.#lotteryTypes = this.#readCSV(csvPath);
this.#purchaseHistory = {};
this.#salesStats = {};

Object.keys(LOTTERY_TYPES).forEach((type) => {
this.#purchaseHistory[type] = [];
this.#salesStats[type] = { count: 0, revenue: 0 };
});
}

#readCSV(filePath) {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.trim().split("\n");
const headers = lines[0].split(",").map((h) => h.trim());

const types = {};
lines.slice(1).forEach((line) => {
if (line.trim() === "") return;

const values = line.split(",").map((v) => v.trim());
const obj = {};
headers.forEach((header, index) => {
obj[header] = values[index];
});
types[obj.type] = obj;
});

return types;
}

getLotteryInfo(type) {
return this.#lotteryTypes[type];
}

purchaseLotteries(type, amount) {
const info = this.#lotteryTypes[type];
const price = Number(info.price);
const count = amount / price;

if (Number(info.stock) < count) {
throw new Error(PLUS_ERROR_MESSAGE.SHORTAGE);
}

const lotteries = [];
for (let i = 0; i < count; i++) {
lotteries.push(this.#generateLottery(type));
}

info.stock = String(Number(info.stock) - count);

this.#purchaseHistory[type].push(...lotteries);

this.#salesStats[type].count += count;
this.#salesStats[type].revenue += amount;

return lotteries;
}

#generateLottery(type) {
if (type === LOTTERY_TYPES.LOTTO) {
const numbers = Random.pickUniqueNumbersInRange(1, 30, 5);
return new LottoLottery(numbers.sort((a, b) => a - b));
}

if (type === LOTTERY_TYPES.PENSION) {
const number = Random.pickNumberInRange(1, 1000000);
return new PensionLottery(number);
}

throw new Error(PLUS_ERROR_MESSAGE.TYPE);
}

getPurchasedLotteries(type) {
return this.#purchaseHistory[type];
}

getSalesStats() {
return this.#salesStats;
}

getStock(type) {
return Number(this.#lotteryTypes[type].stock);
}

addStock(type, quantity) {
const current = Number(this.#lotteryTypes[type].stock);
this.#lotteryTypes[type].stock = String(current + quantity);
}

getAllStocks() {
const stocks = {};
Object.keys(LOTTERY_TYPES).forEach((type) => {
stocks[type] = this.getStock(type);
});
return stocks;
}
}

export default LotteryShop;
45 changes: 45 additions & 0 deletions src/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { LOTTO_CONFIG } from "./constants.js";

class Lotto {
#numbers;

constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
}

#validate(numbers) {
if (numbers.length !== LOTTO_CONFIG.NUMBER_COUNT) {
throw new Error(
`[ERROR] 로또 번호는 ${LOTTO_CONFIG.NUMBER_COUNT}개여야 합니다.`
);
}

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

const scopeCheck = numbers.every(
(num) => num >= LOTTO_CONFIG.MIN_NUMBER && num <= LOTTO_CONFIG.MAX_NUMBER
);
if (!scopeCheck) {
throw new Error(
`[ERROR] 로또 번호는 ${LOTTO_CONFIG.MIN_NUMBER}부터 ${LOTTO_CONFIG.MAX_NUMBER} 사이의 숫자여야 합니다.`
);
}
}

numberCheck() {
return this.#numbers;
}

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

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

export default Lotto;
Loading