diff --git a/README.md b/README.md
index 15bb106b5..2ae0bb34f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,67 @@
-# javascript-lotto-precourse
+## 기능 요구사항 분석
+
+### 입력/출력
+
+**입력**
+
+- [x] 로또 구입 금액을 입력받는다.
+- [x] 당첨 번호와 보너스 번호를 입력받는다.
+- [x] 잘못된 값을 입력했을 경우 메시지와 함께 에러를 발생시킨 후
+해당 지점부터 다시 입력 받는다.
+
+**출력**
+
+- [x] 발행할 로또 개수를 바탕으로 로또 수량 및 번호 리스트들을 출력한다.
+- [x] 당첨 내역을 출력한다.
+- [x] 수익률을 출력한다.
+- [x] 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
+
+
+
+### 기능
+
+**로또 발행**
+
+- [x] 로또 구입 금액만큼 발행할 로또 개수를 정한다.
+- [x] 로또 개수만큼 로또를 발행한다.
+- [x] 발행한 로또 번호를 오름차순으로 정렬한다.
+
+**결과 계산**
+
+- [x] 발행된 로또와 당첨 번호를 비교하여 당첨 내역을 계산한다.
+- [x] 당첨 내역을 바탕으로 총 수익률을 계산한다.
+
+ 수익률은 소수점 둘째 자리에서 반올림한다.
+
+
+
+
+### 에러
+
+**입력값**
+
+- [x] 공백인 경우
+- 로또 구입 금액
+ - [x] 구입 금액의 단위가 1,000으로 나누어 떨어지지 않을 경우
+ - [x] 숫자가 아닌 문자가 포함된 경우
+- 당첨 번호
+ - [x] 구분자가 쉼표(,)가 아닌 경우
+ - [x] 숫자가 아닌 문자가 포함된 경우
+ - [x] 구분자 형식이 잘못된 경우 (e.g. ‘1,2,3,’ ‘1,2,,3’)
+ - [x] 구분자 사이에 공백이 포함된 경우 (e.g. ‘1, 2,3’ ‘1,2 ,3’)
+ - [x] 입력된 숫자가 6개가 아닌 경우
+ - [x] 중복된 번호가 입력된 경우
+ - [x] 1~45 범위의 숫자가 아닌 경우
+- 보너스 번호
+ - [x] 숫자가 아닌 문자가 포함된 경우
+
+**로또 발행**
+
+- [x] 중복된 번호가 발행된 경우
+- [x] 발행된 번호가 6개가 아닌 경우
+
+
+
+### 엣지 케이스
+
+- [x] 당첨되지 않았을 때 (2개 이하로 번호 일치)
\ No newline at end of file
diff --git a/__tests__/InputValidatorTest.js b/__tests__/InputValidatorTest.js
new file mode 100644
index 000000000..6bda359cb
--- /dev/null
+++ b/__tests__/InputValidatorTest.js
@@ -0,0 +1,77 @@
+import { ERROR_MESSAGE, TERMS } from '../src/utils/constants';
+import InputValidator from '../src/utils/InputValidator';
+
+describe('입력값 검증 테스트 (로또 구입 금액)', () => {
+ it.each([
+ ['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT],
+ [
+ '숫자가 아닌 문자가 포함된 경우',
+ 'lotto',
+ ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE,
+ ],
+ [
+ '1,000원으로 나누어 떨어지지 않는 경우',
+ '900',
+ ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT,
+ ],
+ ])('%s', (_, input, expectedError) => {
+ expect(() =>
+ InputValidator.runValidate(TERMS.PURCHASE_AMOUNT, input)
+ ).toThrow(expectedError);
+ });
+});
+
+describe('입력값 검증 테스트 (당첨 번호)', () => {
+ it.each([
+ ['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT],
+ [
+ '구분자가 쉼표(,)가 아닌 경우',
+ '1,2;3,4,5,6',
+ ERROR_MESSAGE.WINNING_NUMBER_TYPE,
+ ],
+ [
+ '구분자 형식이 잘못된 경우',
+ ',1,2,3,4,5,6',
+ ERROR_MESSAGE.WINNING_NUMBER_TYPE,
+ ],
+ [
+ '숫자가 아닌 문자가 포함된 경우',
+ '1,2,h,4,5,6',
+ ERROR_MESSAGE.WINNING_NUMBER_TYPE,
+ ],
+ [
+ '숫자가 6개가 아닌 경우',
+ '1,2,3,4,5',
+ ERROR_MESSAGE.WINNING_NUMBER_LENGTH,
+ ],
+ [
+ '중복된 숫자가 포함된 경우',
+ '1,2,3,4,5,5',
+ ERROR_MESSAGE.WINNING_NUMBER_DUPLICATE,
+ ],
+ [
+ '1~45 범위 밖의 숫자가 포함된 경우',
+ '1,2,3,4,5,50',
+ ERROR_MESSAGE.WINNING_NUMBER_RANGE,
+ ],
+ ])('%s', (_, input, expectedError) => {
+ expect(() =>
+ InputValidator.runValidate(TERMS.WINNING_NUMBER, input)
+ ).toThrow(expectedError);
+ });
+});
+
+describe('입력값 검증 테스트 (보너스 번호)', () => {
+ it.each([
+ ['공백이 입력된 경우', ' ', ERROR_MESSAGE.BLANK_INPUT],
+ [
+ '숫자가 아닌 문자가 포함된 경우',
+ 'lotto',
+ ERROR_MESSAGE.BONUS_NUMBER_TYPE,
+ ],
+ ])('%s', (_, input, expectedError) => {
+ expect(() => InputValidator.runValidate(TERMS.BONUS_NUMBER, input)).toThrow(
+ expectedError
+ );
+ });
+});
diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js
index 409aaf69b..9abfb89f7 100644
--- a/__tests__/LottoTest.js
+++ b/__tests__/LottoTest.js
@@ -1,18 +1,15 @@
-import Lotto from "../src/Lotto";
+import Lotto from '../src/model/Lotto';
-describe("로또 클래스 테스트", () => {
- test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
+describe('로또 클래스 테스트', () => {
+ test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6, 7]);
- }).toThrow("[ERROR]");
+ }).toThrow('[ERROR]');
});
- // TODO: 테스트가 통과하도록 프로덕션 코드 구현
- test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
+ test('로또 번호에 중복된 숫자가 있으면 예외가 발생한다.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 5]);
- }).toThrow("[ERROR]");
+ }).toThrow('[ERROR]');
});
-
- // TODO: 추가 기능 구현에 따른 테스트 코드 작성
});
diff --git a/src/App.js b/src/App.js
index 091aa0a5d..03f4976be 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,80 @@
+import { Console } from '@woowacourse/mission-utils';
+import LottoMachine from './model/LottoMachine.js';
+import { IO_MESSAGE, SEPERATOR, TERMS } from './utils/constants.js';
+import Input from './view/Input.js';
+import Parser from './utils/Parser.js';
+import RankCalculator from './model/RankCalculator.js';
+import StatisticsView from './view/StatisticsView.js';
+
class App {
- async run() {}
+ #issuedLottos = [];
+
+ /**
+ * 1. 구입 금액을 입력받고 숫자로 변환
+ */
+ async getPurchaseAmount() {
+ const purchaseAmountStr = await Input.readInputValues(
+ TERMS.PURCHASE_AMOUNT,
+ IO_MESSAGE.PURCHASE_AMOUNT_INPUT
+ );
+ return Number(purchaseAmountStr);
+ }
+
+ /**
+ * 2. 로또 발행
+ */
+ issueLottos(purchaseAmount) {
+ this.#issuedLottos = LottoMachine.run(purchaseAmount);
+ }
+
+ /**
+ * 3. 발행된 로또 번호를 정렬하여 출력
+ */
+ printIssuedLottos() {
+ this.#issuedLottos.forEach((lotto) => {
+ const numbers = lotto.getNumbers();
+ numbers.sort((a, b) => a - b);
+ Console.print(`[${numbers.join(', ')}]`);
+ });
+ }
+
+ /**
+ * 4. 당첨 번호와 보너스 번호를 입력받고 파싱
+ */
+ async getWinningNumbers() {
+ const winningNumber = await Input.readInputValues(
+ TERMS.WINNING_NUMBER,
+ IO_MESSAGE.WINNING_NUMBER_INPUT
+ );
+ const parsedWinningNumber = Parser.convertToNumberArray(
+ winningNumber,
+ SEPERATOR.COMMA
+ );
+
+ const bonusNumber = await Input.readInputValues(
+ TERMS.BONUS_NUMBER,
+ IO_MESSAGE.BONUS_NUMBER_INPUT
+ );
+
+ return { winningNumber: parsedWinningNumber, bonusNumber };
+ }
+
+ // 전체 프로세스 실행
+ async run() {
+ const purchaseAmount = await this.getPurchaseAmount();
+ this.issueLottos(purchaseAmount);
+ this.printIssuedLottos();
+
+ const { winningNumber, bonusNumber } = await this.getWinningNumbers();
+
+ const rankCounts = RankCalculator.calculate(
+ this.#issuedLottos,
+ winningNumber,
+ bonusNumber
+ );
+
+ StatisticsView.printResult(rankCounts, purchaseAmount);
+ }
}
export default App;
diff --git a/src/Lotto.js b/src/Lotto.js
deleted file mode 100644
index cb0b1527e..000000000
--- a/src/Lotto.js
+++ /dev/null
@@ -1,18 +0,0 @@
-class Lotto {
- #numbers;
-
- constructor(numbers) {
- this.#validate(numbers);
- this.#numbers = numbers;
- }
-
- #validate(numbers) {
- if (numbers.length !== 6) {
- throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
- }
- }
-
- // TODO: 추가 기능 구현
-}
-
-export default Lotto;
diff --git a/src/model/Lotto.js b/src/model/Lotto.js
new file mode 100644
index 000000000..56a4a7c90
--- /dev/null
+++ b/src/model/Lotto.js
@@ -0,0 +1,35 @@
+import { ERROR_MESSAGE, LOTTO_RULES } from '../utils/constants.js';
+
+class Lotto {
+ #numbers;
+
+ constructor(numbers) {
+ this.#validate(numbers);
+ this.#numbers = numbers;
+ }
+
+ #validate(numbers) {
+ if (numbers.length !== LOTTO_RULES.TICKET_NUMBER_COUNT) {
+ throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_COUNT);
+ }
+
+ if (new Set(numbers).size !== numbers.length) {
+ throw new Error(ERROR_MESSAGE.GENERATED_LOTTO_DUPLICATE);
+ }
+ }
+
+ getNumbers() {
+ return [...this.#numbers];
+ }
+
+ calculateMatchCount(winningNumber) {
+ return this.#numbers.filter((number) => winningNumber.includes(number))
+ .length;
+ }
+
+ hasBonusNumber(bonusNumber) {
+ return this.#numbers.includes(Number(bonusNumber));
+ }
+}
+
+export default Lotto;
diff --git a/src/model/LottoMachine.js b/src/model/LottoMachine.js
new file mode 100644
index 000000000..7dbc4d869
--- /dev/null
+++ b/src/model/LottoMachine.js
@@ -0,0 +1,49 @@
+import { Console, MissionUtils } from '@woowacourse/mission-utils';
+import { IO_MESSAGE, LOTTO_RULES } from '../utils/constants.js';
+import Parser from '../utils/Parser.js';
+import Lotto from './Lotto.js';
+
+class LottoMachine {
+ /**
+ * 구입 가능한 로또 티켓 수 계산
+ */
+ static #determineLottoNumber(purchaseAmount) {
+ return Parser.getPurchaseCount(purchaseAmount);
+ }
+
+ /**
+ * 한 개의 로또 번호 생성 (e.g. [8, 21, 23, 41, 42, 43])
+ */
+ static #generateLottoNumbers() {
+ return MissionUtils.Random.pickUniqueNumbersInRange(
+ LOTTO_RULES.MIN_NUMBER,
+ LOTTO_RULES.MAX_NUMBER,
+ LOTTO_RULES.TICKET_NUMBER_COUNT
+ );
+ }
+
+ /**
+ * 로또 티켓 발행
+ */
+ static #issueLottoTickets(purchaseCount) {
+ const issuedLottos = [];
+
+ Array.from({ length: purchaseCount }).forEach(() => {
+ const lottoNumbers = this.#generateLottoNumbers();
+ const lotto = new Lotto(lottoNumbers);
+
+ issuedLottos.push(lotto);
+ });
+
+ return issuedLottos;
+ }
+
+ static run(purchaseAmount) {
+ const purchaseCount = this.#determineLottoNumber(purchaseAmount);
+ Console.print(IO_MESSAGE.PURCHASE_COUNT_OUTPUT(purchaseCount));
+
+ return this.#issueLottoTickets(purchaseCount);
+ }
+}
+
+export default LottoMachine;
diff --git a/src/model/RankCalculator.js b/src/model/RankCalculator.js
new file mode 100644
index 000000000..49c8a5bd3
--- /dev/null
+++ b/src/model/RankCalculator.js
@@ -0,0 +1,41 @@
+import { LOTTO_RULES } from '../utils/constants.js';
+
+class RankCalculator {
+ /**
+ * 일치 결과(일치 개수, 보너스 여부)를 토대로 순위 배열의 인덱스로 반환
+ * @param {number} matchCount - 일치한 번호의 개수
+ * @param {boolean} hasBonus - 보너스 번호 일치 여부
+ * @returns {number | undefined} 통계 배열의 인덱스 (0: 1등, 1: 2등, ... 4: 5등). 꽝은 undefined.
+ */
+ static #getRankIndex(matchCount, hasBonus) {
+ if (matchCount === 5) return hasBonus ? 1 : 2; // 1: 2등 인덱스, 2: 3등 인덱스
+
+ // 일치 개수 : 랭크 인덱스
+ const RANK_INDEX_MAP = {
+ 6: 0, // 1등
+ 4: 3, // 4등
+ 3: 4, // 5등
+ };
+
+ return RANK_INDEX_MAP[matchCount];
+ }
+
+ static calculate(issuedLottos, winningNumber, bonusNumber) {
+ // [1등, 2등, 3등, 4등, 5등] 순서의 당첨 통계 배열
+ // (예: [0, 0, 1, 0, 1] => 3등 1개, 5등 1개)
+ const rankCounts = new Array(LOTTO_RULES.TOTAL_RANK_COUNT).fill(0);
+
+ issuedLottos.forEach((lotto) => {
+ const matchCount = lotto.calculateMatchCount(winningNumber);
+ const hasBonus = lotto.hasBonusNumber(bonusNumber);
+
+ const rankIndex = this.#getRankIndex(matchCount, hasBonus);
+
+ if (rankIndex !== undefined) rankCounts[rankIndex] += 1;
+ });
+
+ return rankCounts;
+ }
+}
+
+export default RankCalculator;
diff --git a/src/utils/InputValidator.js b/src/utils/InputValidator.js
new file mode 100644
index 000000000..034d4dae2
--- /dev/null
+++ b/src/utils/InputValidator.js
@@ -0,0 +1,115 @@
+import { ERROR_MESSAGE, LOTTO_RULES, SEPERATOR, TERMS } from './constants.js';
+import Parser from './Parser.js';
+
+/**
+ * 구입 금액
+ */
+class PurchaseAmountValidator {
+ static #validators = [this.#validateIsNaN, this.#validateIsValidUnit];
+
+ static #validateIsNaN(value) {
+ if (Number.isNaN(Number(value))) {
+ throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_TYPE);
+ }
+ }
+
+ static #validateIsValidUnit(value) {
+ if (value % LOTTO_RULES.TICKET_PRICE !== 0) {
+ throw new Error(ERROR_MESSAGE.PURCHASE_AMOUNT_UNIT);
+ }
+ }
+
+ static validate(value) {
+ this.#validators.forEach((validator) => validator.call(this, value));
+ }
+}
+
+/**
+ * 당첨 번호
+ */
+class WinningNumberValidator {
+ static #validators = [
+ this.#validateHasNaN,
+ this.#validateDuplicate,
+ this.#validateLength,
+ this.#validateRange,
+ ];
+
+ /**
+ * 숫자가 아닌 문자가 포함된 경우, 구분자가 쉼표(,)가 아닌 경우 검증
+ */
+ static #validateHasNaN(value) {
+ if (value.some((number) => Number.isNaN(number))) {
+ throw new Error(ERROR_MESSAGE.WINNING_NUMBER_TYPE);
+ }
+ }
+
+ static #validateLength(value) {
+ if (value.length !== LOTTO_RULES.TICKET_NUMBER_COUNT) {
+ throw new Error(ERROR_MESSAGE.WINNING_NUMBER_LENGTH);
+ }
+ }
+
+ static #validateDuplicate(value) {
+ if (new Set(value).size !== value.length) {
+ throw new Error(ERROR_MESSAGE.WINNING_NUMBER_DUPLICATE);
+ }
+ }
+
+ static #validateRange(value) {
+ if (
+ value.some((number) => number < LOTTO_RULES.MIN_NUMBER) ||
+ value.some((number) => number > LOTTO_RULES.MAX_NUMBER)
+ ) {
+ throw new Error(ERROR_MESSAGE.WINNING_NUMBER_RANGE);
+ }
+ }
+
+ static validate(value) {
+ const parsedValue = Parser.convertToNumberArray(value, SEPERATOR.COMMA);
+
+ this.#validators.forEach((validator) => validator.call(this, parsedValue));
+ }
+}
+
+/**
+ * 보너스 번호
+ */
+class BonusNumberValidator {
+ static #validateIsNaN(value) {
+ if (Number.isNaN(Number(value))) {
+ throw new Error(ERROR_MESSAGE.BONUS_NUMBER_TYPE);
+ }
+ }
+
+ static validate(value) {
+ this.#validateIsNaN(value);
+ }
+}
+
+/**
+ * 입력값 검증
+ */
+class InputValidator {
+ static #validators = {
+ [TERMS.PURCHASE_AMOUNT]: (value) => PurchaseAmountValidator.validate(value),
+ [TERMS.WINNING_NUMBER]: (value) => WinningNumberValidator.validate(value),
+ [TERMS.BONUS_NUMBER]: (value) => BonusNumberValidator.validate(value),
+ };
+
+ // 공통 검증 로직
+ static #commonValidate(input) {
+ if (input.trim().length === 0) {
+ throw new Error(ERROR_MESSAGE.BLANK_INPUT);
+ }
+ }
+
+ static runValidate(key, value) {
+ InputValidator.#commonValidate(value);
+
+ const validator = this.#validators[key];
+ validator(value);
+ }
+}
+
+export default InputValidator;
diff --git a/src/utils/Parser.js b/src/utils/Parser.js
new file mode 100644
index 000000000..cc5759eec
--- /dev/null
+++ b/src/utils/Parser.js
@@ -0,0 +1,29 @@
+import { LOTTO_RULES } from './constants.js';
+
+class Parser {
+ /**
+ * 구입 금액으로 구입가능한 로또 티켓 수 계산
+ * @param {string} purchaseAmount - 구입 금액 문자열 (e.g. '5000')
+ */
+ static getPurchaseCount(purchaseAmount) {
+ return Number(purchaseAmount) / LOTTO_RULES.TICKET_PRICE;
+ }
+
+ /**
+ * 문자열을 구분자로 분리하여 숫자 배열로 변환
+ * @param {string} input - 입력 문자열
+ * @param {string} seperator - 구분자
+ * @returns
+ */
+ static convertToNumberArray(input, seperator) {
+ return input.split(seperator).map((number) => {
+ const trimmed = number.trim();
+
+ if (trimmed === '') return NaN;
+
+ return Number(trimmed);
+ });
+ }
+}
+
+export default Parser;
diff --git a/src/utils/constants.js b/src/utils/constants.js
new file mode 100644
index 000000000..a2b4e169e
--- /dev/null
+++ b/src/utils/constants.js
@@ -0,0 +1,45 @@
+export const IO_MESSAGE = Object.freeze({
+ PURCHASE_AMOUNT_INPUT: '구입금액을 입력해 주세요.\n',
+ WINNING_NUMBER_INPUT: '\n당첨 번호를 입력해 주세요.\n',
+ BONUS_NUMBER_INPUT: '\n보너스 번호를 입력해 주세요.\n',
+
+ PURCHASE_COUNT_OUTPUT: (count) => `\n${count}개를 구매했습니다.`,
+ WINNING_STATISTICS_OUTPUT: '\n당첨 통계\n---\n',
+ TOTAL_PROFIT_OUTPUT: (profitRate) =>
+ `총 수익률은 ${profitRate.toFixed(1)}%입니다.`,
+});
+
+export const ERROR_MESSAGE = Object.freeze({
+ BLANK_INPUT: '[ERROR] 입력값이 비어 있습니다.',
+
+ PURCHASE_AMOUNT_UNIT: '[ERROR] 구입 금액은 1,000원 단위여야 합니다.',
+ PURCHASE_AMOUNT_TYPE: '[ERROR] 구입 금액은 숫자여야 합니다.',
+
+ GENERATED_LOTTO_COUNT: '[ERROR] 로또 번호는 6개여야 합니다.',
+ GENERATED_LOTTO_DUPLICATE: '[ERROR] 로또 번호에 중복된 숫자가 있습니다.',
+
+ WINNING_NUMBER_LENGTH: '[ERROR] 당첨 번호는 6개여야 합니다.',
+ WINNING_NUMBER_TYPE: '[ERROR] 당첨 번호의 입력 형식이 올바르지 않습니다.',
+ WINNING_NUMBER_DUPLICATE: '[ERROR] 당첨 번호에 중복된 숫자가 있습니다.',
+ WINNING_NUMBER_RANGE: '[ERROR] 당첨 번호는 1부터 45 사이의 숫자여야 합니다.',
+
+ BONUS_NUMBER_TYPE: '[ERROR] 보너스 번호는 숫자여야 합니다.',
+});
+
+export const LOTTO_RULES = Object.freeze({
+ TICKET_PRICE: 1000,
+ MIN_NUMBER: 1,
+ MAX_NUMBER: 45,
+ TICKET_NUMBER_COUNT: 6,
+ TOTAL_RANK_COUNT: 5,
+});
+
+export const SEPERATOR = Object.freeze({
+ COMMA: ',',
+});
+
+export const TERMS = Object.freeze({
+ PURCHASE_AMOUNT: 'purchaseAmount',
+ WINNING_NUMBER: 'winningNumber',
+ BONUS_NUMBER: 'bonusNumber',
+});
diff --git a/src/view/Input.js b/src/view/Input.js
new file mode 100644
index 000000000..998d67a1a
--- /dev/null
+++ b/src/view/Input.js
@@ -0,0 +1,29 @@
+import { Console } from '@woowacourse/mission-utils';
+import InputValidator from '../utils/InputValidator.js';
+
+class Input {
+ /**
+ * 올바른 입력을 받을 때까지 반복해서 입력을 요청
+ */
+ static async readValidInput(key, message) {
+ try {
+ const input = await Console.readLineAsync(message);
+ InputValidator.runValidate(key, input);
+
+ return input;
+ } catch (err) {
+ Console.print(err.message);
+ return Input.readValidInput(key, message);
+ }
+ }
+
+ /**
+ * @param {string} key - 검증 키 (e.g. 'purchaseAmount', 'winningNumber')
+ * @param {string} message - 입력 요청 메시지
+ */
+ static readInputValues(key, message) {
+ return Input.readValidInput(key, message);
+ }
+}
+
+export default Input;
diff --git a/src/view/StatisticsView.js b/src/view/StatisticsView.js
new file mode 100644
index 000000000..0c2528f81
--- /dev/null
+++ b/src/view/StatisticsView.js
@@ -0,0 +1,61 @@
+import { Console } from '@woowacourse/mission-utils';
+import { IO_MESSAGE } from '../utils/constants.js';
+
+const PRIZE_INFO = [
+ { description: '6개 일치', prize: 2_000_000_000 },
+ { description: '5개 일치, 보너스 볼 일치', prize: 30_000_000 },
+ { description: '5개 일치', prize: 1_500_000 },
+ { description: '4개 일치', prize: 50_000 },
+ { description: '3개 일치', prize: 5_000 },
+];
+
+class StatisticsView {
+ /**
+ * 당첨 통계 헤더를 출력합니다.
+ */
+ static #printStatisticsHeader() {
+ Console.print(IO_MESSAGE.WINNING_STATISTICS_OUTPUT);
+ }
+
+ /**
+ * 개별 당첨 내역을 출력
+ */
+ static #printRankDetails(rankCounts) {
+ // 5등(index 4)부터 1등(index 0) 순서로 출력하기 위해 역순으로 순회
+ for (let i = PRIZE_INFO.length - 1; i >= 0; i--) {
+ const { description, prize } = PRIZE_INFO[i];
+ const count = rankCounts[i];
+ const prizeString = prize.toLocaleString('ko-KR');
+
+ Console.print(`${description} (${prizeString}원) - ${count}개`);
+ }
+ }
+
+ /**
+ * 총 수익률을 계산하고 출력합니다.
+ */
+ static #printProfitRate(rankCounts, purchaseAmount) {
+ // 총 상금 계산
+ const totalProfit = rankCounts.reduce((sum, count, index) => {
+ return sum + count * PRIZE_INFO[index].prize;
+ }, 0);
+
+ // 수익률 계산 ( (총상금 / 구매금액) * 100 )
+ const profitRate = (totalProfit / purchaseAmount) * 100;
+
+ Console.print(IO_MESSAGE.TOTAL_PROFIT_OUTPUT(profitRate));
+ }
+
+ /**
+ * 당첨 통계 전체 결과를 출력합니다.
+ * @param {number[]} rankCounts - [1등, 2등, 3등, 4등, 5등] 당첨 개수 배열
+ * @param {number} purchaseAmount - 총 구매 금액
+ */
+ static printResult(rankCounts, purchaseAmount) {
+ this.#printStatisticsHeader();
+ this.#printRankDetails(rankCounts);
+ this.#printProfitRate(rankCounts, purchaseAmount);
+ }
+}
+
+export default StatisticsView;