Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2b967f5
docs: README 기능 목록 작성
gustn99 Oct 29, 2025
a5b47ce
refactor: Lotto를 도메인 폴더로 이동
gustn99 Oct 30, 2025
4f8b839
feat(Lotto): 중복 예외 검증 로직 추가
gustn99 Oct 30, 2025
2db839c
feat(Lotto): 숫자 범위 검증 로직 추가
gustn99 Oct 30, 2025
6db0fac
feat(Lotto): format 메서드 구현
gustn99 Oct 30, 2025
98cf9c7
feat(Lottos): Lottos 클래스 생성
gustn99 Oct 30, 2025
75369b2
feat(Lottos): format 메서드 구현
gustn99 Oct 30, 2025
d13b7c1
refactor(LottoTest): format 테스트 메시지를 명확하게 수정
gustn99 Oct 30, 2025
00d12e3
fix(test): Lotto, Lottos 테스트 파일명 서로 변경
gustn99 Oct 30, 2025
4e79321
refactor(Lottos): lottos getter를 퍼블릭으로 변경
gustn99 Oct 30, 2025
c73bf51
feat(Lottos): win 메서드 구현
gustn99 Oct 30, 2025
093f316
feat(Lottos): calculateTotalReturn 메서드 구현
gustn99 Oct 30, 2025
6980ef8
refactor(constant): 구매 단위(1000)을 상수로 분리
gustn99 Oct 30, 2025
da72329
refactor(constant): 로또 관련 상수 분리
gustn99 Oct 30, 2025
220983d
feat(DrawnNumbers): DrawnNumbers 클래스 생성
gustn99 Oct 31, 2025
31a54e9
refactor(DrawnNumbers): 당첨 번호 검증 및 변환도 DrawnNumbers의 책임으로 분리
gustn99 Oct 31, 2025
bbe3342
feat(DrawnNumbers): matchCount 메서드 구현
gustn99 Oct 31, 2025
4df638e
refactor(DrawnNumbers): matchCount 로직을 Lotto 클래스로 이동
gustn99 Oct 31, 2025
c427abb
feat(LottoDrawer): LottoDrawer 클래스 생성
gustn99 Oct 31, 2025
06d2a93
feat(DrawnNumbers): calculateRank 메서드 추가
gustn99 Nov 1, 2025
6effb06
refactor(constants): 등수 표기를 상수로 분리
gustn99 Nov 1, 2025
7338f71
refactor(LottosTest): 적절한 object 순회 메서드 사용
gustn99 Nov 1, 2025
915aa9e
feat(LottoDrawer): LottoDrawer 클래스 생성
gustn99 Nov 1, 2025
9cfdbcd
feat(InputView): InputView 클래스 구현
gustn99 Nov 1, 2025
4fb78bc
feat(OutputView): OutputView 클래스 구현
gustn99 Nov 1, 2025
4cbb0b8
feat(LottoController): LottoController 클래스 구현
gustn99 Nov 1, 2025
5c4aa7b
style: import 시 확장자 추가
gustn99 Nov 3, 2025
c190203
feat(Lottos): formatResult 메서드 추가
gustn99 Nov 3, 2025
ec90ebb
fix(Lottos): 수익률을 소숫점 둘째 자리에서 반올림하도록 수정
gustn99 Nov 3, 2025
55667ab
refactor(DrawnNumbers): etc 속성 제거
gustn99 Nov 3, 2025
c436478
refactor(LottoDrawer): lottos를 필드에서 제거
gustn99 Nov 3, 2025
aa57a7c
feat(LottoController): LottoController 구현
gustn99 Nov 3, 2025
045ee9e
feat(Formatter): Formatter 클래스 생성
gustn99 Nov 3, 2025
1a6610a
refactor(constant): 인풋 메시지 상수화
gustn99 Nov 3, 2025
3246ebc
refactor(LottoController): 결과 출력 메서드 세분화
gustn99 Nov 3, 2025
87319e2
refactor(LottoController): 입력 구문 모듈화
gustn99 Nov 3, 2025
389c2ea
refactor(constant): rank 관련 상수 파일명 수정
gustn99 Nov 3, 2025
3ee0d81
feat(utils): 공통 에러 prefix 처리 함수 생성
gustn99 Nov 3, 2025
6291c1d
refactor(constant): 에러 메시지 상수화
gustn99 Nov 3, 2025
d149d7b
refactor(RANK): key값이 영어로 시작하도록 수정
gustn99 Nov 3, 2025
16984f8
refactor(DrawnNumbers): rank 상수 적용
gustn99 Nov 3, 2025
744131a
docs(README): 폴더구조 추가
gustn99 Nov 3, 2025
2ae2b68
feat(LottoTest): 로또 번호 개수가 6개 미만인 경우 테스트 추가
gustn99 Nov 3, 2025
85af63d
style: js 확장자 추가
gustn99 Nov 3, 2025
a9213fe
fix: if문 중괄호 제거
gustn99 Nov 3, 2025
41d069f
fix: 출력 형식 수정
gustn99 Nov 3, 2025
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
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
# javascript-lotto-precourse

## 기능 개요

1. 로또 구매
2. 자동 로또 생성
3. 로또 추첨
4. 당첨 결과 및 수익률 출력

## 세부 기능 목록

1. 구매 금액 입력
- 빈 문자열 예외
- 1000원 단위가 아니면 예외

<br/>

2. 구매 금액을 기준으로 로또 번호 생성
3. 구매한 로또 개수와 생성된 로또 번호 출력

<br/>

4. 당첨 번호 입력
- 빈 문자열 예외
- 번호가 쉼표로 구분되지 않으면 예외
- 쉼표로 구분된 번호가 6개가 아니면 예외
- 1-45 사이 숫자가 아니면 예외
- 중복 숫자가 있으면 예외
5. 보너스 번호 입력
- 빈 문자열 예외
- 1-45 사이 숫자가 아니면 예외
- 당첨 번호 중 하나라도 중복되면 예외

<br/>

6. 랜덤 생성된 로또 번호들마다 당첨 여부 확인
7. 총 수익률 계산
8. 당첨 통계(1-5등 개수, 총 수익률) 출력

## 폴더 구조

```bash
src
├─ constants
│ ├─ errorMessages.js
│ ├─ inputMessages.js
│ ├─ lotto.js
│ ├─ rank.js
│ └─ unit.js
├─ controller
│ └─ LottoController.js // 전체 로또 추첨 흐름 제어
├─ domains
│ └─ Lotto.js // 단일 로또 상태 관리
├─ models
│ ├─ DrawnNumbers.js // 당첨 번호 + 보너스 번호 상태 관리
│ ├─ LottoDrawer.js // DrawnNumbers -> Lottos 상태 업데이트
│ └─ Lottos.js // 사용자 로또 번호 상태 관리
├─ utils
│ ├─ error.js
│ └─ Formatter.js // 출력문 형식 관리
└─ view
├─ InputView.js // 사용자 입력 처리
└─ OutputView.js // 출력 처리
```
101 changes: 101 additions & 0 deletions __tests__/DrawnNumbersTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
BONUS_NUMBER_ERROR_MESSAGES,
WINNING_NUMBER_ERROR_MESSAGES,
} from "../src/constants/errorMessages.js";
import { RANK } from "../src/constants/rank.js";
import Lotto from "../src/domains/Lotto.js";
import DrawnNumbers from "../src/models/DrawnNumbers.js";

describe("DrawnNumbers 클래스", () => {
describe("생성자 테스트", () => {
test("당첨 번호 입력이 없으면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers("", "1");
}).toThrow(WINNING_NUMBER_ERROR_MESSAGES.NONEMPTY);
});

test("당첨 번호 입력이 공백이면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers(" ", "1");
}).toThrow(WINNING_NUMBER_ERROR_MESSAGES.NONEMPTY);
});

test("당첨 번호가 쉼표 외 구분자로 분리되어 있으면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers("1. 2. 3. 4. 5. 6", "1");
}).toThrow(WINNING_NUMBER_ERROR_MESSAGES.DELIMITER);
});

test("보너스 번호 입력이 없으면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers("1, 2, 3, 4, 5, 6", "");
}).toThrow(BONUS_NUMBER_ERROR_MESSAGES.NONEMPTY);
});

test("보너스 번호 입력이 공백이면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers("1, 2, 3, 4, 5, 6", " ");
}).toThrow(BONUS_NUMBER_ERROR_MESSAGES.NONEMPTY);
});

test("보너스 번호가 1보다 작으면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers("1, 2, 3, 4, 5, 6", "0");
}).toThrow(BONUS_NUMBER_ERROR_MESSAGES.MIN_VALUE);
});

test("보너스 번호가 45보다 크면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers("1, 2, 3, 4, 5, 6", "46");
}).toThrow(BONUS_NUMBER_ERROR_MESSAGES.MAX_VALUE);
});

test("보너스 번호가 이미 당첨 번호에 포함되어 있으면 예외가 발생한다.", () => {
expect(() => {
new DrawnNumbers("1, 2, 3, 4, 5, 6", "1");
}).toThrow(BONUS_NUMBER_ERROR_MESSAGES.UNIQUE);
});
});

describe("calculateRank 메서드 테스트", () => {
test("당첨 번호와 로또 번호가 6개 모두 일치하는 경우 1등을 반환한다.", () => {
const lottoInstance = new Lotto([1, 2, 3, 4, 5, 6]);
const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7");
expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe(
RANK.FIRST
);
});

test("당첨 번호와 로또 번호가 5개 일치하고, 로또 번호에 보너스 번호가 포함되어 있는 경우 2등을 반환한다.", () => {
const lottoInstance = new Lotto([1, 2, 3, 4, 5, 7]);
const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7");
expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe(
RANK.SECOND
);
});

test("당첨 번호와 로또 번호가 5개 일치하는 경우 3등을 반환한다.", () => {
const lottoInstance = new Lotto([1, 2, 3, 4, 5, 8]);
const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7");
expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe(
RANK.THIRD
);
});

test("당첨 번호와 로또 번호가 4개 일치하는 경우 4등을 반환한다.", () => {
const lottoInstance = new Lotto([1, 2, 3, 4, 7, 8]);
const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7");
expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe(
RANK.FOURTH
);
});

test("당첨 번호와 로또 번호가 3개 일치하는 경우 5등을 반환한다.", () => {
const lottoInstance = new Lotto([1, 2, 3, 7, 8, 9]);
const drawnNumbersInstance = new DrawnNumbers("1,2,3,4,5,6", "7");
expect(drawnNumbersInstance.calculateRank(lottoInstance)).toBe(
RANK.FIFTH
);
});
});
});
68 changes: 56 additions & 12 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,62 @@
import Lotto from "../src/Lotto";
import { LOTTO_ERROR_MESSAGES } from "../src/constants/errorMessages.js";
import Lotto from "../src/domains/Lotto.js";

describe("로또 클래스 테스트", () => {
test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6, 7]);
}).toThrow("[ERROR]");
describe("Lotto 클래스", () => {
describe("생성자 테스트", () => {
test("로또 번호의 개수가 6개 미만이면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5]);
}).toThrow(LOTTO_ERROR_MESSAGES.LENGTH);
});

test("로또 번호의 개수가 6개를 넘어가면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6, 7]);
}).toThrow(LOTTO_ERROR_MESSAGES.LENGTH);
});

test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 5]);
}).toThrow(LOTTO_ERROR_MESSAGES.UNIQUE);
});

test("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([0, 2, 3, 4, 5, 6]);
}).toThrow(LOTTO_ERROR_MESSAGES.MIN_VALUE);
});

test("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 46]);
}).toThrow(LOTTO_ERROR_MESSAGES.MAX_VALUE);
});
});

// TODO: 테스트가 통과하도록 프로덕션 코드 구현
test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 5]);
}).toThrow("[ERROR]");
describe("compare 메서드 테스트", () => {
test("두 로또 인스턴스 사이의 공통 원소 개수를 반환한다.", () => {
const lotto1 = new Lotto([1, 3, 5, 7, 9, 11]);
const lotto2 = new Lotto([1, 2, 3, 4, 5, 6]);
expect(lotto2.compare(lotto1)).toBe(3);
});

test("두 로또 인스턴스 사이에 공통 원소가 없으면 0을 반환한다.", () => {
const lotto1 = new Lotto([1, 3, 5, 7, 9, 11]);
const lotto2 = new Lotto([2, 4, 6, 8, 10, 12]);
expect(lotto2.compare(lotto1)).toBe(0);
});
});

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
describe("includes 메서드 테스트", () => {
test("로또 인스턴스에 인자값이 포함되어 있으면 true를 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
expect(lotto.includes(1)).toBe(true);
});

test("로또 인스턴스에 인자값이 포함되어 있지 않으면 false를 반환한다.", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
expect(lotto.includes(7)).toBe(false);
});
});
});
62 changes: 62 additions & 0 deletions __tests__/LottosTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { RANK, RANK_TO_PRIZE_MAP } from "../src/constants/rank.js";
import { PURCHASE_UNIT } from "../src/constants/unit.js";
import Lotto from "../src/domains/Lotto.js";
import Lottos from "../src/models/Lottos.js";

const PURCHASE_COUNT = 3;

describe("Lottos 클래스", () => {
describe("생성자 테스트", () => {
test("입력 개수만큼의 lottos 배열을 생성한다.", () => {
const lottosInstance = new Lottos(PURCHASE_COUNT);
const lottos = lottosInstance.getLottos();
expect(lottos).toHaveLength(PURCHASE_COUNT);
});

test("lottos 배열은 Lotto 인스턴스로 구성된다.", () => {
const lottosInstance = new Lottos(PURCHASE_COUNT);
const lottos = lottosInstance.getLottos();
lottos.forEach((lotto) => {
expect(lotto).toBeInstanceOf(Lotto);
});
});
});

describe("win 메서드 테스트", () => {
test("순위에 따라 등수 통계를 업데이트한다.", () => {
Object.values(RANK).forEach((rank) => {
const lottosInstance = new Lottos(PURCHASE_COUNT);
lottosInstance.win(rank);
const ranks = lottosInstance.getRanks();
expect(ranks[rank]).toBe(1);
});
});

test("순위에 따라 총 상금을 업데이트한다.", () => {
Object.entries(RANK_TO_PRIZE_MAP).forEach(([rank, prize]) => {
const lottosInstance = new Lottos(PURCHASE_COUNT);
lottosInstance.win(rank);
const totalPrize = lottosInstance._getTotalPrize();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지난 미션에서 아영 님께 제가 받았던 리뷰와 반대 상황이네요!
당시 제가 내부 동작 자체를 명확히 확인해야 한다면, private 필드를 직접 노출하지 않는 선에서 getter 메서드를 두는 방법도 있겠다고 했었는데, 테스트를 위해 내부 상태를 노출하는 것은 안티 패턴이 될 수도 있다고 하더라고요..😅 작성한 테스트가 동작이 아닌 구현을 검증하고 있는지 확인해 보는 것이 중요한 것 같습니다

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TDD를 시도하면서 단순히 메서드 구현 이전의 과정으로써 테스트를 작성했던 게 가장 큰 오류 같네요 🥲 성급한 적용이었던 것 같습니다. TDD의 단위를 어떻게 가져가야 할지 조금 감이 잡힐 것 같아요! 좋은 피드백 감사합니다~

expect(totalPrize).toBe(prize);
});
});
});

describe("calculateTotalReturn 메서드 테스트", () => {
test("상금을 수익률 형태로 변환해 반환한다.", () => {
Object.entries(RANK_TO_PRIZE_MAP).forEach(([rank, prize]) => {
const lottosInstance = new Lottos(PURCHASE_COUNT);
lottosInstance.win(rank);

const purchaseAmount = PURCHASE_COUNT * PURCHASE_UNIT;
const expectedTotalPrize = prize;
const expectedTotalReturn = (
expectedTotalPrize / purchaseAmount
).toFixed(1);

const totalReturn = lottosInstance.calculateTotalReturn();
expect(totalReturn).toBe(expectedTotalReturn);
});
});
});
});
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();
lottoController.run();
}
}

export default App;
18 changes: 0 additions & 18 deletions src/Lotto.js

This file was deleted.

42 changes: 42 additions & 0 deletions src/constants/errorMessages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { error } from "../utils/error.js";
import { LOTTO_MAX_VALUE, LOTTO_SIZE } from "./lotto.js";
import { PURCHASE_UNIT } from "./unit.js";

export const PURCHASE_ERROR_MESSAGES = {
NONEMPTY: error("구입 금액을 입력해 주세요."),
UNIT: error(`구입 금액은 ${PURCHASE_UNIT}원 단위로 입력해야 합니다.`),
};

export const LOTTO_ERROR_MESSAGES = {
UNIQUE: error("로또 번호는 중복될 수 없습니다."),
LENGTH: error(`로또 번호는 ${LOTTO_SIZE}개여야 합니다.`),
MIN_VALUE: error(
`로또 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어라.. 여기는 LOTTO_MIN_VALUE여야 할 것 같아요 😅

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 그러네요,,,😓

),
MAX_VALUE: error(
`로또 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.`
),
};

export const WINNING_NUMBER_ERROR_MESSAGES = {
NONEMPTY: error("당첨 번호를 입력해 주세요."),
DELIMITER: error("당첨 번호는 쉼표로 구분되어야 합니다."),
};

export const BONUS_NUMBER_ERROR_MESSAGES = {
NONEMPTY: error("보너스 번호를 입력해 주세요."),
MIN_VALUE: error(
`보너스 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.`
),
MAX_VALUE: error(
`보너스 번호는 ${LOTTO_MAX_VALUE}에서 ${LOTTO_MAX_VALUE} 사이의 숫자여야 합니다.`
),
UNIQUE: error("이미 당첨 번호에 포함된 번호입니다."),
};

export const ERROR_MESSAGES = {
PURCHASE: PURCHASE_ERROR_MESSAGES,
LOTTO: LOTTO_ERROR_MESSAGES,
WINNING_NUMBER: WINNING_NUMBER_ERROR_MESSAGES,
BONUS_NUMBER: BONUS_NUMBER_ERROR_MESSAGES,
};
5 changes: 5 additions & 0 deletions src/constants/inputMessages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const INPUT_MESSAGES = {
PURCHASE_AMOUNT: "구입금액을 입력해 주세요.",
WINNING_NUMBER: "당첨 번호를 입력해 주세요.",
BONUS_NUMBER: "보너스 번호를 입력해 주세요.",
};
3 changes: 3 additions & 0 deletions src/constants/lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const LOTTO_MIN_VALUE = 1;
export const LOTTO_MAX_VALUE = 45;
export const LOTTO_SIZE = 6;
Loading