Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8f05464
chore(types): 타입 선언 파일 추가
YounaJ00 Nov 2, 2025
80cf7e5
docs(readme): 로또 기능 목록 추가
YounaJ00 Nov 2, 2025
1160ac4
feat(lotto): 로또 번호 범위 및 중복 검증 추가
YounaJ00 Nov 2, 2025
8c2b3d3
feat(app): 구입 금액 입력 및 기본 검증 로직 추가
YounaJ00 Nov 2, 2025
87c9b9f
feat(app): 구입 금액에 따른 로또 발행 및 번호 출력 기능 추가
YounaJ00 Nov 2, 2025
d438f68
chore(types): Random 타입 정의 추가
YounaJ00 Nov 2, 2025
0881663
feat(app): 당첨 번호와 보너스 번호 입력, 재입력 처리 추가
YounaJ00 Nov 2, 2025
081f18a
feat(app): 당첨 통계 및 수익률 계산 기능 추가
YounaJ00 Nov 3, 2025
379eceb
test(lotto): 금액, 당첨번호, 보너스번호 재입력 케이스 추가
YounaJ00 Nov 3, 2025
75477a8
fix(app): 수익률 출력 형식 개선
YounaJ00 Nov 3, 2025
4676e41
fix(app): 입력 후 한 줄 공백 추가로 출력 형식 개선
YounaJ00 Nov 3, 2025
85d88a6
refactor(app): 당첨 번호 파싱 및 검증 로직을 함수 분리로 개선
YounaJ00 Nov 3, 2025
a025804
test(lotto): Lotto 클래스 단위 테스트 추가
YounaJ00 Nov 3, 2025
739027a
refactor(app): 개선된 변수명 사용 및 코드 가독성 향상
YounaJ00 Nov 3, 2025
e68d447
refactor(constants): 로또 관련 상수 및 당첨 금액 테이블 분리
YounaJ00 Nov 3, 2025
7890fa8
refactor(constants): 에러 메시지를 상수로 분리
YounaJ00 Nov 3, 2025
8b3e560
refactor(constants): 입력 메시지 상수 분리
YounaJ00 Nov 3, 2025
1aa4cd7
refactor(utils): 공용 출력 유틸 함수 printNewLine 분리
YounaJ00 Nov 3, 2025
7ee3783
refactor(app): 입력 처리와 비즈니스 로직 분리
YounaJ00 Nov 3, 2025
f26d1ab
docs(readme): 메서드 시그니처 제거
YounaJ00 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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,66 @@
# javascript-lotto-precourse

# 🎰 로또

## 📋 구현 기능 목록

---

### 1️⃣ 로또 구입 금액 입력

- [x] 금액 입력 받기
- [x] 1,000원 단위인지 검증
- [x] 1,000원 미만 또는 1,000으로 나누어떨어지지 않으면 `[ERROR]` 출력 후 **해당 입력부터 다시 받기**

---

### 2️⃣ 로또 발행

- [x] 구입 금액 ÷ 1,000 = 발행 개수 계산
- [x] 랜덤으로 로또 번호 생성
- [x] 1개 로또는 중복 없는 6개 번호
- [x] 번호는 **오름차순 정렬**해서 보관
- [x] `N개를 구매했습니다.` 출력 후 각 로또 번호를 `[1, 2, 3, 4, 5, 6]` 형식으로 출력

---

### 3️⃣ 당첨 번호 입력

- [x] 당첨 번호 입력 받기
- [x] 쉼표(,) 기준으로 6개인지 검증
- [x] 각 숫자가 1~45 범위인지 검증
- [x] 중복 숫자 없도록 검증
- [x] 잘못되면 `[ERROR] ...` 출력 후 **당첨 번호부터 다시 받기**

---

### 4️⃣ 보너스 번호 입력

- [x] 보너스 번호 입력 받기
- [x] 1~45 범위 검증
- [x] 당첨 번호와 **겹치지 않도록** 검증
- [x] 잘못되면 `[ERROR] ...` 출력 후 **보너스 번호부터 다시 받기**

---

### 5️⃣ 당첨 통계 계산

- [x] 사용자가 구매한 모든 로또와 당첨 번호를 비교
- [x] 일치 개수에 따라 3개/4개/5개/5개+보너스/6개 집계
- [x] 등수별 상금 합계 계산

---

### 6️⃣ 수익률 계산 및 출력

- [x] 총 상금 ÷ 구입 금액 × 100
- [x] 소수점 둘째 자리에서 반올림해 **소수 첫째 자리까지만** 출력 (예: `62.5%`)
- [x] `총 수익률은 62.5%입니다.` 형식으로 출력

---

### 7️⃣ 예외 처리 및 재입력

- [x] 잘못된 값 입력 시 `[ERROR]`로 시작하는 메시지를 출력
- [x] **예외를 던지되**(throw), `App`에서 잡아서 해당 단계만 다시 입력
- [x] `process.exit()` 사용하지 않음
52 changes: 52 additions & 0 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,56 @@ describe("로또 테스트", () => {
test("예외 테스트", async () => {
await runException("1000j");
});

test("구입 금액이 1000원 단위가 아니면 다시 입력을 받는다", async () => {
const logSpy = getLogSpy();
mockRandoms([
[1, 2, 3, 4, 5, 6],
[7, 8, 9, 10, 11, 12],
[13, 14, 15, 16, 17, 18],
[19, 20, 21, 22, 23, 24],
[25, 26, 27, 28, 29, 30],
]);
mockQuestions(["8500", "2000", "1,2,3,4,5,6", "7"]);

const app = new App();
await app.run();

expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining("[ERROR] 구입 금액은 1,000원 단위여야 합니다.")
);
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining("2개를 구매했습니다.")
);
});

test("당첨 번호가 6개가 아니면 다시 입력을 받는다", async () => {
const logSpy = getLogSpy();
mockRandoms([[1, 2, 3, 4, 5, 6]]);
mockQuestions(["1000", "1,2,3,4,5", "1,2,3,4,5,6", "7"]);

const app = new App();
await app.run();

expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining("[ERROR] 당첨 번호는 6개여야 합니다.")
);
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("당첨 통계"));
});

test("보너스 번호가 당첨 번호와 중복되면 다시 입력을 받는다", async () => {
const logSpy = getLogSpy();
mockRandoms([[1, 2, 3, 4, 5, 6]]);
mockQuestions(["1000", "1,2,3,4,5,6", "3", "7"]);

const app = new App();
await app.run();

expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining(
"[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."
)
);
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("당첨 통계"));
});
Copy link

Choose a reason for hiding this comment

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

재입력에 대한 테스트 너무좋네요!

});
13 changes: 12 additions & 1 deletion __tests__/LottoTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,16 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});

// TODO: 추가 기능 구현에 따른 테스트 코드 작성
test("로또 번호가 6개 보다 적으면 예외가 발생한다.", () => {
expect(() => new Lotto([1, 2, 3, 4, 5]).toThrow("[ERROR]"));
});

test("로또 번호가 1~45 범위를 벗어나면 예외가 발생한다.", () => {
expect(() => new Lotto([0, 2, 3, 4, 5, 6])).toThrow("[ERROR]");
expect(() => new Lotto([1, 2, 3, 4, 5, 100])).toThrow("[ERROR]");
});

test("유효한 로또 번호 6개라면 예외가 발생하지 않는다.", () => {
expect(() => new Lotto([1, 2, 3, 4, 5, 6])).not.toThrow();
});
});
34 changes: 33 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import { LOTTO_PRICE } from "./constants/constants.js";
import InputHandler from "./InputHandler.js";
import LottoService from "./LottoService.js";
import { printNewLine } from "./utils/output.js";

class App {
async run() {}
#inputHandler;
#lottoService;

constructor() {
this.#inputHandler = new InputHandler();
this.#lottoService = new LottoService();
}

async run() {
const purchaseAmount = await this.#inputHandler.readPurchaseAmount();
const ticketCount = purchaseAmount / LOTTO_PRICE;
printNewLine();
Copy link

Choose a reason for hiding this comment

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

printNewLine 도 좋지만
아예 OutputHandler를 만들어서 공통화 해보는것은 어떨까요 ??
매번 쓸필요도 없을것 같다는 느낌이 들어서요 !


const tickets = this.#lottoService.publishLottos(ticketCount);
this.#lottoService.printTickets(tickets);

const winningNumbers = await this.#inputHandler.readWinningNumbers();
const bonusNumber = await this.#inputHandler.readBonusNumber(
winningNumbers
);

const stats = this.#lottoService.calculateStats(
tickets,
winningNumbers,
bonusNumber
);
this.#lottoService.printStats(stats, purchaseAmount);
}
Comment on lines +15 to +34

Choose a reason for hiding this comment

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

run() 안에 있는 흐름을 메소드로 분리시켜서 아래와 같이 해보면 어떨까요?

async run() {
  const purchaseAmount = await this.#readPurchaseAmount();
  const tickets = this.#publishTickets(purchaseAmount);
  const { winningNumbers, bonusNumber } = await this.#readWinningInputs();
  const stats = this.#calculateStatistics(tickets, winningNumbers, bonusNumber);
  this.#printStatistics(stats, purchaseAmount);
}

async #readPurchaseAmount() {
  const purchaseAmount = await this.#inputHandler.readPurchaseAmount();
  printNewLine();
  return purchaseAmount;
}

#publishTickets(purchaseAmount) {
  const ticketCount = purchaseAmount / LOTTO_PRICE;
  const tickets = this.#lottoService.publishLottos(ticketCount);
  this.#lottoService.printTickets(tickets);
  return tickets;
}

async #readWinningInputs() {
  const winningNumbers = await this.#inputHandler.readWinningNumbers();
  const bonusNumber = await this.#inputHandler.readBonusNumber(winningNumbers);
  return { winningNumbers, bonusNumber };
}

#calculateStatistics(tickets, winningNumbers, bonusNumber) {
  return this.#lottoService.calculateStats(tickets, winningNumbers, bonusNumber);
}

#printStatistics(stats, purchaseAmount) {
  this.#lottoService.printStats(stats, purchaseAmount);
}

}

export default App;
117 changes: 117 additions & 0 deletions src/InputHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Console } from "@woowacourse/mission-utils";
import {
LOTTO_COUNT,
LOTTO_MAX,
LOTTO_MIN,
LOTTO_PRICE,
NUMBER_REGEX,
} from "./constants/constants.js";
import { ERROR_MESSAGES, INPUT_MESSAGES } from "./constants/messages.js";
import { printNewLine } from "./utils/output.js";

class InputHandler {
async readPurchaseAmount() {
while (true) {
const input = await Console.readLineAsync(INPUT_MESSAGES.PURCHASE_AMOUNT);
try {
return this.#parsePurchaseAmount(input);
} catch (error) {
Console.print(error.message);
}
}
}
Comment on lines +13 to +22

Choose a reason for hiding this comment

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

App.js에서 readPurchaseAmount()를 사용할 때 인스턴스 변수로 저장하여 사용하지만 readPurchaseAmount()는 매번 같은 인스턴스를 사용하는 형태로 보이고 cocnstructor가 불필요해보이네요! static 메서드로 인스턴스 변수 저장 없이 InputHandler.readPurchaseAmount() 형식으로 상용할 수 있을 것 같아요!


#trimInput(input) {
return (input ?? "").trim();
}

#parseNumber(input, errorMessage = ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE) {
if (!NUMBER_REGEX.test(input)) {
throw new Error(errorMessage);
}
return Number(input);
}

#parsePurchaseAmount(input) {
const trimmedInput = this.#trimInput(input);
const amount = this.#parseNumber(
trimmedInput,
ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_NUMBER
);

if (amount < LOTTO_PRICE) {
throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_TOO_LOW);
}
if (amount % LOTTO_PRICE !== 0) {
throw new Error(ERROR_MESSAGES.PURCHASE_AMOUNT_NOT_DIVISIBLE);
}
Comment on lines +42 to +47

Choose a reason for hiding this comment

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

이곳 말고도 에러 메시지와 로직 결합 완화가 필요해 보입니다!

각 검증 함수에서 에러 메시지를 직접 지정하고 있다. 메시지 상수는 좋지만, 파라미터를 통해 주입하는 대신 내부에서 하드코딩하는 부분이 많은듯 해요.

에러 유형별로 ValidationError 클래스를 따로 두거나, 메시지 맵을 한 곳에서 관리하면 코드 일관성이 더 좋아질 것 같습니다!

return amount;
}

async readWinningNumbers() {
printNewLine();
while (true) {
const input = await Console.readLineAsync(INPUT_MESSAGES.WINNING_NUMBERS);
try {
return this.#parseWinningNumbers(input);
} catch (error) {
Console.print(error.message);
}
}
}

#parseWinningNumbers(input) {
const trimmedInput = this.#trimInput(input);
const parts = trimmedInput.split(",").map((value) => value.trim());
if (parts.length !== LOTTO_COUNT) {
throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_COUNT_INVALID);
}

const numbers = parts.map((numberString) =>
this.#parseNumber(numberString)
);
this.#validateLottoNumbers(numbers);

const unique = new Set(numbers);
if (unique.size !== LOTTO_COUNT) {
throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_DUPLICATE);
}
return numbers;
}
Comment on lines +63 to +80

Choose a reason for hiding this comment

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

#parseWinningNumbers가 입력 다듬기, 분리, 변환, 검증까지 모두 맡고 있는 것 같아요.

#splitAndTrimInput(문자열 처리)과 #validateLottoNumbers(검증)를 분리해 단일 책임 원칙을 적용하면 각 함수의 목적이 더 명확해질듯 합니다!


#validateLottoNumbers(numbers) {
if (
!numbers.every(
(number) =>
Number.isInteger(number) && number >= LOTTO_MIN && number <= LOTTO_MAX
)
) {
throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE);
}
}

async readBonusNumber(winningNumbers) {
printNewLine();
while (true) {
const input = await Console.readLineAsync(INPUT_MESSAGES.BONUS_NUMBER);
try {
return this.#parseBonusNumber(input, winningNumbers);
} catch (error) {
Console.print(error.message);
}
}
}

#parseBonusNumber(input, winningNumbers) {
const trimmedInput = this.#trimInput(input);
const bonus = this.#parseNumber(trimmedInput);
this.#validateLottoNumbers([bonus]);

if (winningNumbers.includes(bonus)) {
throw new Error(ERROR_MESSAGES.BONUS_NUMBER_DUPLICATE);
}
return bonus;
}
}

export default InputHandler;
23 changes: 19 additions & 4 deletions src/Lotto.js

Choose a reason for hiding this comment

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

this.#numbers = numbers;처럼 그냥 할당하는 방식은 외부에서 전달받은 배열을 직접 참조하는 방식이기 때문에 외부에서 수정이 가능한 상태라 의도치 않은 side effect를 유발할 수 있다고 합니다.

저도 다른 분 코드리뷰 하면서 배우게 된 내용인데
#62

[...numbers] 와 같은 형태로 복사하여 저장하는 방식이 안전하다고 하네요ㅎㅎ

Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { LOTTO_COUNT, LOTTO_MAX, LOTTO_MIN } from "./constants/constants.js";
import { ERROR_MESSAGES } from "./constants/messages.js";

class Lotto {
#numbers;

Expand All @@ -7,12 +10,24 @@ class Lotto {
}

#validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
if (numbers.length !== LOTTO_COUNT) {
throw new Error(ERROR_MESSAGES.LOTTO_NUMBERS_COUNT_INVALID);
}

if (
!numbers.every(
(number) =>
Number.isInteger(number) && number >= LOTTO_MIN && number <= LOTTO_MAX
)
) {
throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE);
}
}

// TODO: 추가 기능 구현
const uniqueCount = new Set(numbers).size;
if (uniqueCount !== LOTTO_COUNT) {
throw new Error(ERROR_MESSAGES.LOTTO_NUMBERS_DUPLICATE);
}
}
Copy link

Choose a reason for hiding this comment

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

validate 로직이 다소 가독성이 떨어지는것 같긴합니다!
증복된 validate도 있을수 있으니 util한 validate와 도메인에
결합되는 validate를 분리해보면 어떨까요!

}

export default Lotto;
Loading