Skip to content
138 changes: 137 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,137 @@
# javascript-planetlotto-precourse
# 🪐 javascript-planetlotto-precourse
우테코 로또 발매기인 행성 로또를 구현한다.

---
## 📝 구현 기능 목록

### 1. 로또 구입 금액 입력 받기
- [x] `구입금액을 입력해 주세요.` 출력
- [x] 잘못된 입력값일 경우 throw문을 통해 예외 발생

- [예외] 입력값이 없을 경우
- [예외] 숫자가 아닌 값을 입력했을 경우 (문자, 기호)
- [예외] 소수를 입력했을 경우
- [예외] 음수 또는 0을 입력했을 경우
- [예외] 공백이 있을 경우
- [예외] 500원으로 나누어 떨어지지 않는 경우
<br>

> 테스트 목록
> - [ ] 입력값이 500원 단위일 때 올바르게 저장되는가?
> - [ ] 구입금액이 500원 단위가 아닐 때 예외가 발생한다.
> - [ ] 입력값이 없을 시 예외가 발생한다.
> - [ ] 숫자가 아닌 입력(문자, 기호) 시 예외가 발생한다.
> - [ ] 소수 입력 시 예외가 발생한다.
> - [ ] 음수 또는 0 입력 시 예외가 발생한다.
> - [ ] 공백 입력 시 예외가 발생한다.
<br>

### 2. 발행한 로또 수량 출력하기
- [x] 발행한 로또 수량 구하기 (로또 구입 금액 / 500)
- [ ] `2개를 구매했습니다.` 출력
<br>

> 테스트 목록
> - [ ] 구입금액에 따라 로또 수량이 올바르게 계산된다.
<br>

### 3. 발행한 로또 번호 출력하기
- [ ] 로또 발행하기

- 중복되지 않는 5개의 숫자 뽑기 (Random 값 추출은 `Random. pickUniqueNumbersInRange()` 활용)
- 로또 번호는 오름차순으로 정렬
- 발행한 로또 수량만큼 반복
<br>

> 테스트 목록
> - [ ] 한 장의 로또 번호가 5개인지 확인한다.
> - [ ] 5개의 번호 중 중복된 숫자가 없는지 확인한다.
> - [ ] 5개의 번호가 오름차순으로 정렬되는지 확인한다.
> - [ ] 모든 숫자가 1~30 범위인지 확인한다.
> - [ ] 발행된 로또 수량이 구입 수량과 일치하는가?
<br>

### 4. 당첨 번호 입력 받기
- [x] `당첨 번호를 입력해 주세요.` 출력
- [x] 쉼표(,)를 기준으로 번호 구분하기
- [x] 잘못된 입력값일 경우 throw문을 통해 예외 발생

- [예외] 입력값이 없을 경우
- [예외] 공백이 있을 경우
- [예외] 1 ~ 30 범위의 정수가 아닐 경우
- [예외] 중복되는 숫자가 있을 경우
- [예외] 5개가 아닐 경우
<br>

> 테스트 목록
> - [ ] 입력값이 없을 시 예외가 발생한다.
> - [ ] 문자, 기호, 소수 입력 시 예외가 발생한다. (*쉼표 제외)
> - [ ] 공백 입력 시 예외가 발생한다.
> - [ ] 1 ~ 30 범위를 벗어난 숫자 입력 시 예외가 발생한다.
> - [ ] 중복된 숫자 입력 시 예외가 발생한다.
> - [ ] 당첨 번호의 개수가 5개가 아닐 시 예외가 발생한다.
<br>

### 5. 보너스 번호 입력 받기
- [x] `보너스 번호를 입력해 주세요.` 출력
- [x] 잘못된 입력값일 경우 throw문을 통해 예외 발생

- [예외] 입력값이 없을 경우
- [예외] 숫자가 아닌 값을 입력했을 경우 (문자, 기호)
- [예외] 소수를 입력했을 경우
- [예외] 음수를 입력했을 경우
- [예외] 공백이 있을 경우
- [예외] 1 ~ 30 범위의 숫자가 아닐 경우
- [예외] 당첨 번호와 중복될 경우
<br>

> 테스트 목록
> - [ ] 유효한 숫자 입력 시 올바르게 저장되는지 확인한다.
> - [ ] 입력값이 없을 시 예외가 발생한다.
> - [ ] 숫자가 아닌 입력(문자, 기호) 시 예외가 발생한다.
> - [ ] 소수 입력 시 예외가 발생한다.
> - [ ] 공백 입력 시 예외가 발생한다.
> - [ ] 1 ~ 30 범위를 벗어난 숫자 입력 시 예외가 발생한다.
> - [ ] 당첨 번호와 중복되는 숫자 입력 시 예외가 발생한다.
<br>

### 6. 당첨 내역 출력하기
- [x] 사용자가 구매한 로또 번호와 당첨 번호 비교하기
- [ ] 당첨 내역 출력하기
<br>

> 테스트 목록
> - [ ] 각 로또 번호와 당첨 번호 일치 개수를 정확히 계산한다.
> - [ ] 5개 번호 일치 시 1등으로 계산된다.
> - [ ] 4개 번호 일치 + 보너스 번호 포함 시 2등으로 계산된다.
> - [ ] 4개 번호 일치 시 3등으로 계산된다.
> - [ ] 3개 번호 일치 + 보너스 번호 일치 시 4등으로 계산된다.
> - [ ] 2개 번호 일치 + 보너스 번호 일시 시 5등으로 계산된다.
> - [ ] 일치 개수에 따라 통계 객체가 올바르게 갱신된다.
<br>

### 7. 에러 메시지 출력 후 재입력 받기
- [x] 사용자가 잘못된 값을 입력할 경우 [ERROR]로 시작하는 메시지를 출력
- [x] 해당 입력 단계부터 다시 입력 받기
<br>

---
## 기능 요구 사항
본 프로그램의 목적은 기능 구현에 그치는 것이 아니라 프리코스에서 학습한 개발 방식(문제 분해, 설계, TDD)이 코드에 드러나는 것에 있다.

- [x] 입력/출력 역할은 제공된 InputView, OutputView에서 수행하며 기존 메서드를 수정, 삭제할 수 없다.

- [x] 로또 번호의 숫자 범위는 1~30까지이다.
- [x] 1개의 로또를 발행할 때 중복되지 않는 5개의 숫자를 뽑는다.
- [x] 당첨 번호 추첨 시 중복되지 않는 숫자 5개와 보너스 번호 1개를 뽑는다.
- [x] 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
- [x] 1등: 5개 번호 일치 / 100,000,000원
- [x] 2등: 4개 번호 + 보너스 번호 일치 / 10,000,000원
- [x] 3등: 4개 번호 일치 / 1,500,000원
- [x] 4등: 3개 번호 일치 + 보너스 번호 일치 / 500,000원
- [x] 5등: 2개 번호 일치 + 보너스 번호 일치 / 5,000원
- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
- [x] 로또 1장의 가격은 500원이다.
- [x] 당첨 번호와 보너스 번호를 입력받는다.
- [ ] 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역을 출력하고 로또 게임을 종료한다.
- [x] 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시키고 해당 메시지를 출력한 다음 해당 지점부터 다시 입력을 받는다.
54 changes: 53 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
import { InputView, OutputView } from "./view.js";
import { InputValidator } from "./validator/InputValidator.js";
import LottoMachine from "./model/LottoMachine.js";
import { create2DArray } from "./util/create2DArray.js";

class App {
async run() {}
async run() {
const purchaseAmount = await this.getPurchaseAmount();
const lottoMachine = new LottoMachine(Number(purchaseAmount));
const lottos = create2DArray(lottoMachine.get2DArray());
OutputView.printPurchasedLottos(lottos);

const winningNumbers = await this.getWinningNumbers();
const bonusNumber = await this.getBonusNumber(winningNumbers);

const result = lottoMachine.calculateResult(winningNumbers, bonusNumber);
OutputView.printResult(result);
}

async getPurchaseAmount() {
while (true) {
try {
const purchaseAmount = await InputView.askAmount();
InputValidator.validatePurchaseAmount(purchaseAmount);
return purchaseAmount;
} catch(e) {
OutputView.printErrorMessage(e.message);
}
}
}

async getWinningNumbers() {
while (true) {
try {
const input = await InputView.askWinningLotto();
const winningNumbers = InputValidator.validateWinningNumbers(input);
return winningNumbers;
} catch (e) {
OutputView.printErrorMessage(e.message);
}
}
}

async getBonusNumber(winningNumbers) {
while (true) {
try {
const input = await InputView.askBonusNumber();
const bonusNumber = InputValidator.validateBonusNumber(input, winningNumbers);
return bonusNumber;
} catch (e) {
OutputView.printErrorMessage(e.message);
}
}
}
}

export default App;
7 changes: 7 additions & 0 deletions src/constant/lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const LOTTO_CONFIG = Object.freeze({
RANDOM_MIN: 1,
RANDOM_MAX: 30,
RANDOM_COUNT: 5,
LOTTO_PRICE: 500,
DELIMITER: ',',
});
29 changes: 29 additions & 0 deletions src/constant/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const ERROR_MESSAGE = Object.freeze({
OUT_OF_RANGE: '로또 번호는 1부터 30의 숫자여야 합니다.',
INVALID_COUNT: '로또 번호는 5개여야 합니다.',
DUPLICATE_NUMBER: "로또 번호는 중복될 수 없습니다.",

PURCHASE: {
EMPTY_INPUT: '구입금액을 입력해 주세요.',
NOT_NUMBER: '구입금액은 숫자여야 합니다.',
NOT_INTEGER: '구입금액은 정수여야 합니다.',
NOT_POSITIVE: '구입금액은 음수가 될 수 없습니다.',
NOT_MULTIPLE_UNIT: '구입금액은 500원 단위로 입력해야 합니다.',
},

WINNING_NUMBERS: {
OUT_OF_RANGE: '당첨 번호는 1부터 30의 숫자여야 합니다.',
INVALID_COUNT: '당첨 번호는 5개여야 합니다.',
DUPLICATE_NUMBER: "당첨 번호는 중복될 수 없습니다.",
},

BONUS: {
EMPTY_INPUT: '보너스 번호를 입력해 주세요.',
NOT_NUMBER: '보너스 번호는 숫자여야 합니다.',
NOT_INTEGER: '보너스 번호는 정수여야 합니다.',
OUT_OF_RANGE: '보너스 번호는 1부터 30의 숫자여야 합니다.',
DUPLICATE_WITH_WINNING: '보너스 번호는 당첨 번호와 중복될 수 없습니다.',
}
});


31 changes: 31 additions & 0 deletions src/model/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { validate } from "../util/validate.js";
import { ERROR_MESSAGE } from "../constant/message.js";

class Lotto {
#numbers;

constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers.slice().sort((a, b) => a - b);
}

#validate(numbers) {
if (!validate.hasValidCount(numbers)) {
throw new Error(ERROR_MESSAGE.INVALID_COUNT);
}

if (validate.hasDuplicate(numbers)) {
throw new Error(ERROR_MESSAGE.DUPLICATE_NUMBER);
}

if (!validate.areNumbersInRange(numbers)) {
throw new Error(ERROR_MESSAGE.OUT_OF_RANGE);
}
}

getNumbers() {
return this.#numbers;
}
}

export default Lotto;
93 changes: 93 additions & 0 deletions src/model/LottoMachine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Lotto from './Lotto.js';
import { getRandomLottoNumbers } from '../util/random.js';
import { LOTTO_CONFIG } from '../constant/lotto.js';

class LottoMachine {
#lottos = [];
#purchaseAmount;
#lottoCount;

constructor(purchaseAmount) {
this.#purchaseAmount = purchaseAmount;
this.#lottoCount = Math.floor(purchaseAmount / LOTTO_CONFIG.LOTTO_PRICE);
this.#generateLotto();
}

#generateLotto() {
for (let i = 0; i < this.#lottoCount; i += 1) {
const numbers = getRandomLottoNumbers();
this.#lottos.push(new Lotto(numbers));
}
}

get2DArray(r, c) {
const lottoArr = [];
for (let i = 0 ; i < r ; i++) {
const temp = [];
this.#generateLotto();
result.push(temp);
}
return lottoArr;
}

getLottoCount() {
return this.#lottoCount;
}

getLottos() {
return this.#lottos;
}

calculateResult(winningNumbers, bonusNumber) {
const result = this.#initResult();
this.#lottos.forEach(lotto => {
const matchCount = this.#countMatches(lotto, winningNumbers);
const hasBonus = this.#hasBonus(lotto, bonusNumber);
this.#updateResult(result, matchCount, hasBonus);
});
return result;
}

#initResult() {
return {
ZERO: 0,
FIRST: 0,
SECOND: 0,
THIRD: 0,
FOURTH: 0,
FIFTH: 0,
};
}

#countMatches(lotto, winningNumbers) {
const numbers = lotto.getNumbers();
const matchedNumbers = numbers.filter(num => winningNumbers.includes(num));
return matchedNumbers.length;
}

#hasBonus(lotto, bonusNumber) {
return lotto.getNumbers().includes(bonusNumber);
}

#updateResult(result, matchCount, hasBonus) {
if (matchCount === 5) result.FIRST += 1;
else if (matchCount === 4 && hasBonus) result.SECOND += 1;
else if (matchCount === 4) result.THIRD += 1;
else if (matchCount === 3 && hasBonus) result.FOURTH += 1;
else if (matchCount === 2 && hasBonus) result.FIFTH += 1;
else if (matchCount === 0) result.ZERO +=1;
}

buildStatistics(result) {
return [
{ match: 0, prize: 0, count: result.ZERO },
{ match: 2, prize: 5000, count: result.FIFTH, bonus: true },
{ match: 3, prize: 500000, count: result.FOURTH, bonus: true },
{ match: 4, prize: 1500000, count: result.THIRD },
{ match: 4, prize: 10000000, count: result.SECOND, bonus: true },
{ match: 5, prize: 100000000, count: result.FIRST },
];
}
}

export default LottoMachine;
10 changes: 10 additions & 0 deletions src/util/create2DArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function create2DArray(lottos) {
const lottoArr = [];

lottos.forEach((lotto) => {
lotto.getNumbers();
lottoArr.push();
});

return lottoArr;
}
7 changes: 7 additions & 0 deletions src/util/parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LOTTO_CONFIG } from "../constant/lotto.js";

export function parseLottoNumbers(input) {
const splitted = input.split(LOTTO_CONFIG.DELIMITER);
const numbers = splitted.map(n => Number(n.trim()));
return numbers;
}
9 changes: 9 additions & 0 deletions src/util/random.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { MissionUtils } from '@woowacourse/mission-utils';
import { LOTTO_CONFIG } from '../constant/lotto.js';

export const getRandomLottoNumbers = () =>
MissionUtils.Random.pickUniqueNumbersInRange(
LOTTO_CONFIG.RANDOM_MIN,
LOTTO_CONFIG.RANDOM_MAX,
LOTTO_CONFIG.RANDOM_COUNT
);
Loading