Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
afb0285
docs: 기능목록과 도전목록(예정) 작성
SeongUk52 Jan 10, 2026
4720af8
docs: 기능목록과 도전목록(예정) 작성
SeongUk52 Jan 10, 2026
3ccdb2f
Merge remote-tracking branch 'origin/SeongUk52' into SeongUk52
SeongUk52 Jan 10, 2026
f12e860
feat: 로또 번호 생성
SeongUk52 Jan 10, 2026
3424788
feat: 로또 구입 금액에 따라 로또 생성
SeongUk52 Jan 10, 2026
8b109d9
feat: 당첨번호와 보너스 번호에 따른 등수 반환
SeongUk52 Jan 10, 2026
fe5243e
fix: 로또 번호 반환값 수정
SeongUk52 Jan 10, 2026
856a274
fix: 잘못 연결된 클래스 수정
SeongUk52 Jan 10, 2026
13f30ae
fix: 로또번호가 제대로 오름차순 정렬되도록 수정
SeongUk52 Jan 10, 2026
bbe5ba6
feat: 모든 로또의 등수 목록 Map으로 변환, 컨트롤러 뷰 서비스 연결
SeongUk52 Jan 10, 2026
a8b952b
feat: 구입금액 검증
SeongUk52 Jan 10, 2026
e2899f8
feat: 로또 번호 범위 검증
SeongUk52 Jan 10, 2026
3f08f0c
feat: 로또 번호 범위 검증
SeongUk52 Jan 10, 2026
396580c
Merge remote-tracking branch 'origin/SeongUk52' into SeongUk52
SeongUk52 Jan 10, 2026
131fb1d
feat: 로또 번호 개수 검증
SeongUk52 Jan 10, 2026
8714b77
feat: 보너스 번호 범위 검증
SeongUk52 Jan 10, 2026
685c640
feat: 로또번호, 보너스번호 중복검증
SeongUk52 Jan 10, 2026
37cb817
refactor: 에러 메시지 분리
SeongUk52 Jan 10, 2026
ea4b416
docs: 구현한 기능 목록과 문제에서 주어진 기능 분리
SeongUk52 Jan 10, 2026
e564156
refactor: 승리 조건을 상수화
SeongUk52 Jan 10, 2026
2d50f74
refactor: 로또 가격 상수 외부 분리
SeongUk52 Jan 10, 2026
48c37e2
refactor: 등수 조건 개수 하드코딩된 값 대신 상수사용
SeongUk52 Jan 10, 2026
4e39b26
refactor: 로또 숫자 범위 상수로 분리
SeongUk52 Jan 10, 2026
8d7ea56
fix: 잘못된 상수 변경
SeongUk52 Jan 10, 2026
010ee1f
refactor: 로또 개수 상수화 및 하드코딩 제거
SeongUk52 Jan 10, 2026
14e55df
refactor: 최소 구입 금액 조건 상수화
SeongUk52 Jan 10, 2026
4347434
refactor: 로또 번호 정렬조건 상수화
SeongUk52 Jan 10, 2026
8c1bfb4
chore: 파라미터, 리턴값 주석 추가
SeongUk52 Jan 10, 2026
7ffe834
style: 파라미터, 리턴값 주석 추가
SeongUk52 Jan 10, 2026
1dcf715
Merge remote-tracking branch 'origin/SeongUk52' into SeongUk52
SeongUk52 Jan 10, 2026
9602166
fix
SeongUk52 Jan 10, 2026
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

## 전체 기능목록
### 입력
- [x] 로또 구입 금액을 입력
- [x] 당첨 번호 입력
- [x] 보너스 번호 입력

### 출력
- [x] 발행한 로또 수량 및 번호를 출력
- [x] 당첨 통계 출력
- [x] 예외 발생시 에러 문구 출력

### 검증
- [x] 구입 금액, 로또 번호, 보너스 번호가 숫자인지 검증(문제에서 주어진 기능)

### 뷰는 이미 문제에 구현되어 있으므로 이를 활용해서 나머지를 구현해야함! 뷰에서 요구하는파라미터를 제공해야한다

## 실제 구현한 기능 목록

### 검증
- [x] 구입 금액 양수 or 0인지 검증
- [x] 당첨 번호가 1~30사이의 정수인지 검증
- [x] 로또 번호가 5개인지 검증
- [x] 보너스 번호가 1~30 사이의 정수인지 검증
- [x] 로또 번호와 보너스 번호에 중복이 없는지 검증


### 로또번호
- [x] 로또 번호는 5개의 중복되지 않는 1~30사이의 정렬된 랜덤으로 중복없이 생성된 숫자를 가짐
- [x] 로또 구입 금액에 따라 로또를 생성
- [x] 당첨번호와 보너스 번호를 입력받아서 등수를 반환
- [x] 모든 로또의 등수 목록(countsByRank)를 뷰에서 원하는 형식에 맞춰 반환

## 도전목록 (15:46 시작 종료시간 17:00)

최종테스트에서 도전은 리팩토링과 확장성을 고려한 하드코딩된 값 제거에 초점을 두었습니다.
승리 조건을 상수로 관리하여 핵심 로직(도메인, 리포지토리, 서비스)에 손대지 않고도 상수파일 변경만으로 승리 조건을 바꿀 수 있게 했습니다.

### 리팩토링 도전 (하드코딩 제거)
- [x] 에러 메시지 목록 파일로 분리 및 상수처럼 사용
- [x] 승리 조건 상수화 하고 중복제거 및 확장성 향상(승리조건 변형 가능)
- [ ] 확장성을 고려하여 승리 조건을 컨트롤러에 주입하여 사용(보류, 필수아닌거같고 시간 오래걸리고 코드 지저분해짐)
- [x] 로또 가격 상수 분리
- [x] 로또 숫자 범위 상수 분리
- [x] 로또 개수 상수 분리
- [x] 최소구매금액 상수 분리(기본 0)
- [x] 로또번호 정렬 조건 상수화 (기본 오름차순, 함수의 상수화)
27 changes: 27 additions & 0 deletions __tests__/LottoRepositoryTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import LottoRepository from "../src/repository/LottoRepository";
import Lotto from "../src/domain/Lotto.js";

describe("로또 리포지토리 테스트", () => {
test("발행한 로또 목록을 저장한다.", () => {
const repository = new LottoRepository();
const lotto = new Lotto([1, 2, 3, 4, 5]);

repository.save(lotto);

const lottos = repository.findAllAsNumbers();
expect(lottos).toHaveLength(1);
expect(lottos[0]).toBe(lotto.getNumbers());
});

test("발행한 로또 개수를 조회한다.", () => {
const repository = new LottoRepository();
const lotto1 = new Lotto([1, 2, 3, 4, 5]);
const lotto2 = new Lotto([7, 8, 9, 10, 11]);

repository.save(lotto1);
repository.save(lotto2);

expect(repository.findAllAsNumbers().length).toBe(2);
});
});

19 changes: 19 additions & 0 deletions __tests__/LottoServiceTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import LottoService from "../src/service/LottoService.js";
import LottoRepository from "../src/repository/LottoRepository.js";

describe("당첨 계산 서비스 테스트", () => {
test("구매한 금액에 따른 로또 개수가 생성됐는지 테스트", () => {
const lottoRepository = new LottoRepository();
const service = new LottoService(
lottoRepository
);

service.purchase(1500);


const purchasedLottos = lottoRepository.findAllAsNumbers();

expect(purchasedLottos).toHaveLength(3);
});

});
29 changes: 29 additions & 0 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Lotto from "../src/domain/Lotto.js";

describe("로또 클래스 테스트", () => {
test("로또 번호가 내림차순으로 들어와도 오름차순으로 정렬된다.", () => {
const lotto = new Lotto([45, 44, 43, 42, 41]);
expect(lotto.getNumbers()).toEqual([41, 42, 43, 44, 45]);
});

test("로또 번호가 무작위 순서로 들어와도 오름차순으로 정렬된다.", () => {
const lotto = new Lotto([3, 1, 5, 2, 4]);
expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5]);
});

test("등수 테스트 3등", () => {
const lotto = new Lotto([3, 1, 5, 2, 4]);

const rank = lotto.calculateRanking([1,2,3,4,7], 8);

expect(rank).toBe(3);
});

test("등수 테스트 2등", () => {
const lotto = new Lotto([3, 1, 5, 2, 4]);

const rank = lotto.calculateRanking([1,2,3,4,7], 5);

expect(rank).toBe(2);
});
});
46 changes: 46 additions & 0 deletions __tests__/LottoValidatorTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import LottoValidator from "../src/util/LottoValidator.js";

describe("검증 테스트", () => {
let lottoValidator;

beforeEach(() => {
lottoValidator = new LottoValidator();
});

test("구입금액이 음수일 경우 예외발생", () => {
expect(() => {
lottoValidator.validateAmount(-1000);
}).toThrow();
});

test("로또번호 범위를 벗어날 경우 예외 발생", () => {
expect(() => {
lottoValidator.validateLottoNumbers([1,31,2,3,4]);
}).toThrow();
})

test("로또번호 개수가 틀릴 경우 예외 발생", () => {
expect(() => {
lottoValidator.validateLottoNumbers([1,6,2,3,4,5]);
}).toThrow();
})

test("보너스번호 범위를 벗어날 경우 예외 발생", () => {
expect(() => {
lottoValidator.validateBonusNumber(31,[1,2,3,4,5]);
}).toThrow();
})

test("중복된 번호가 있을 경우 예외 발생", () => {
expect(() => {
lottoValidator.validateDuplicate([1,2,3,5,5]);
}).toThrow();
})

test("보너스번호가 로또 목록에 있을 경우 예외 발생", () => {
expect(() => {
lottoValidator.validateBonusNumber(3,[1,2,3,4,5]);
}).toThrow();
})
});

7 changes: 6 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import LottoController from "./controller/LottoController.js";

class App {
async run() {}
async run() {
const lottoController = new LottoController();
await lottoController.run();
}
}

export default App;
12 changes: 12 additions & 0 deletions src/config/ErrorMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import OptionalConstant from "./OptionalConstant.js";

class ErrorMessage{
static POSITIVE = `구입 금액은 ${OptionalConstant.AMOUNT_CONDITION}이상이어야합니다.`;
static DUPLICATE = '중복된 번호가 있으면 안 됩니다.';
static BONUS_DUPLICATE = '보너스 번호는 당첨번호와 중복되면 안 됩니다.';
static COUNT = `로또 번호는 ${OptionalConstant.LOTTO_COUNT}개여야합니다.`;
static RANGE =
`번호는 ${OptionalConstant.LOTTO_NUMBER_RANGE.MIN}~${OptionalConstant.LOTTO_NUMBER_RANGE.MAX} 사이의 정수여야합니다.`;
}

export default ErrorMessage;
25 changes: 25 additions & 0 deletions src/config/OptionalConstant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class OptionalConstant{
//등수별 맞춘 개수
static WINNING_CONDITION ={
1: {WINNING_NUMBERS: 5, HAS_BONUS: false},
2: {WINNING_NUMBERS: 4, HAS_BONUS: true},
3: {WINNING_NUMBERS: 4, HAS_BONUS: false},
4: {WINNING_NUMBERS: 3, HAS_BONUS: true},
5: {WINNING_NUMBERS: 2, HAS_BONUS: true}
}

static LOTTO_PRICE = 500;

static LOTTO_NUMBER_RANGE = {
MIN : 1,
MAX : 30
}

static LOTTO_COUNT = 5;

static AMOUNT_CONDITION = 0;

static SORT_CONDITION = (a, b) => a - b;
}

export default OptionalConstant
94 changes: 94 additions & 0 deletions src/controller/LottoController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import LottoService from "../service/LottoService.js";
import {InputView, OutputView} from "../view.js";
import LottoValidator from "../util/LottoValidator.js";

class LottoController {
#service;
#inputView;
#outputView;
#validator;

/**
* @param service
* @param inputView
* @param outputView
* @param validator
*/
constructor(service = new LottoService(), inputView = InputView,
outputView = OutputView, validator = new LottoValidator()) {
this.#service = service;
this.#inputView = inputView;
this.#outputView = outputView;
this.#validator = validator;
}

/**
*
* @returns {Promise<void>}
*/
async run() {
await this.#tryAskAmountAndPurchaseLottos();
const lottos = this.#service.getAllLottoNumbers();
this.#outputView.printPurchasedLottos(lottos);
const countByRank = await this.#tryAskWinningResultAndGetCountByRank();
this.#outputView.printResult(countByRank);
}

/**
*
* @returns {Promise<void>}
*/
async #tryAskAmountAndPurchaseLottos() {
try {
const amount = await this.#inputView.askAmount();
this.#validator.validateAmount(amount);
this.#service.purchase(amount);
} catch (error) {
this.#outputView.printErrorMessage(error.message);
await this.#tryAskAmountAndPurchaseLottos();
}
}

/**
*
* @returns {Promise<Map<any, any>>}
*/
async #tryAskWinningResultAndGetCountByRank() {
const winningLotto = await this.#tryAskWinningLotto();
const bonusNumber = await this.#tryAskBonusNumber(winningLotto);
return this.#service.getCountByRankAsMap(winningLotto, bonusNumber);
}

/**
*
* @returns {Promise<number[]>}
*/
async #tryAskWinningLotto() {
try {
const winningLotto = await this.#inputView.askWinningLotto();
this.#validator.validateLottoNumbers(winningLotto);
return winningLotto;
} catch (error) {
this.#outputView.printErrorMessage(error.message);
await this.#tryAskWinningLotto();
}
}

/**
*
* @param winningLotto
* @returns {Promise<number>}
*/
async #tryAskBonusNumber(winningLotto) {
try {
const bonusNumber = await this.#inputView.askBonusNumber();
this.#validator.validateBonusNumber(bonusNumber, winningLotto);
return bonusNumber;
} catch (error) {
this.#outputView.printErrorMessage(error.message);
await this.#tryAskBonusNumber(winningLotto);
}
}
}

export default LottoController;
54 changes: 54 additions & 0 deletions src/domain/Lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import OptionalConstant from "../config/OptionalConstant.js";

class Lotto {
#numbers;

/**
*
* @param numbers
*/
constructor(numbers) {
this.#numbers = numbers.sort(OptionalConstant.SORT_CONDITION);
}

/**
*
* @returns {*}
*/
getNumbers() {
return this.#numbers;
}

/**
*
* @param numbers
* @param bonus
* @returns {number}
*/
calculateRanking(numbers, bonus) {
const {count, isBonus} = this.#countSameAndBonus(numbers, bonus);
for(let i = 1; i < Object.keys(OptionalConstant.WINNING_CONDITION).length + 1; i++) {
if(count === OptionalConstant.WINNING_CONDITION[i].WINNING_NUMBERS &&
isBonus === OptionalConstant.WINNING_CONDITION[i].HAS_BONUS) return i;
}
return 0;
}

/**
*
* @param numbers
* @param bonus
* @returns {{count: number, isBonus: boolean}}
*/
#countSameAndBonus(numbers, bonus) {
let count = 0;
let isBonus = false;
this.#numbers.forEach(v => {
if(numbers.includes(v)) count++;
if(bonus === v) isBonus = true;
})
return {count: count, isBonus: isBonus};
}
}

export default Lotto;
Loading