diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000..fb72f754c8
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,31 @@
+export default {
+ env: {
+ browser: true,
+ es2021: true,
+ },
+ extends: ['airbnb-base', 'prettier'],
+ overrides: [
+ {
+ env: {
+ jest: true,
+ node: true,
+ },
+ files: [
+ '.eslintrc.{js,cjs}',
+ ],
+ parserOptions: {
+ sourceType: 'script',
+ },
+ },
+ ],
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ rules: {
+ 'max-depth': ['error', 2],
+ 'max-params': ['error', 3],
+ 'max-lines-per-function': ['error', { max: 10 }],
+ 'import/extensions': 'off',
+ },
+};
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000000..1b4ce05b28
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,11 @@
+export default {
+ singleQuote: true,
+ semi: true,
+ useTabs: false,
+ tabWidth: 2,
+ trailingComma: "all",
+ printWidth: 80,
+ bracketSpacing: true,
+ arrowParens: "always",
+ endOfLine: "auto",
+};
diff --git a/__tests__/InputManagerTest.js b/__tests__/InputManagerTest.js
new file mode 100644
index 0000000000..3c1318aa27
--- /dev/null
+++ b/__tests__/InputManagerTest.js
@@ -0,0 +1,52 @@
+import InputManager from "../src/InputManager.js";
+import { Console } from "@woowacourse/mission-utils"
+
+jest.mock("@woowacourse/mission-utils", () => ({
+ Console: {
+ readLineAsync: jest.fn()
+ }
+}));
+
+describe("InputManager class", () => {
+ let inputManager;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ inputManager = new InputManager();
+ });
+
+ describe("T-1-1 금액 입력 메세지 출력", () => {
+ test("사용자에게 금액 입력 메세지를 출력해야 한다", async () => {
+ Console.readLineAsync.mockResolvedValue("1000");
+ await inputManager.enterAmount();
+ expect(Console.readLineAsync).toHaveBeenCalledWith("구입금액을 입력해 주세요.\n");
+ });
+ });
+
+ describe("T-1-2 금액 입력 처리", () => {
+ test("입력받은 금액이 1,000원 단위일 때 올바르게 처리해야 한다", async () => {
+ const validAmount = "8000";
+ Console.readLineAsync.mockResolvedValue(validAmount);
+
+ const amount = await inputManager.enterAmount();
+ expect(Console.readLineAsync).toHaveBeenCalled();
+ expect(amount).toBe(parseInt(validAmount, 10));
+ });
+ });
+
+ describe("T-1-3 금액 입력 예외 처리", () => {
+ test("금액이 숫자가 아닌 경우 예외를 던져야 한다", async () => {
+ const invalidAmount = "abc";
+ Console.readLineAsync.mockResolvedValue(invalidAmount);
+
+ await expect(inputManager.enterAmount()).rejects.toThrow("[ERROR] 구입 금액은 숫자여야 합니다.");
+ });
+
+ test("1,000원으로 나누어 떨어지지 않을 경우 예외를 던져야 한다", async () => {
+ const invalidAmount = "1500";
+ Console.readLineAsync.mockResolvedValue(invalidAmount);
+
+ await expect(inputManager.enterAmount()).rejects.toThrow("[ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다.");
+ });
+ });
+});
\ No newline at end of file
diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js
index 97bd457659..14281fcf5b 100644
--- a/__tests__/LottoTest.js
+++ b/__tests__/LottoTest.js
@@ -14,5 +14,33 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});
- // 아래에 추가 테스트 작성 가능
+ describe("T-2-1 구입 금액에 따른 로또 발행 테스트", () => {
+ test("입력받은 금액에 따라 적절한 수의 로또가 발행되어야 한다", () => {
+ const amount = 5000;
+ const lottos = Lotto.generateMultipleLottos(amount);
+ expect(lottos).toHaveLength(amount / 1000);
+ });
+ });
+
+ describe("T-2-2 로또 번호 중복 없이 생성 테스트", () => {
+ test("각 로또 번호는 6개이며 서로 중복되지 않아야 한다", () => {
+ const numbers = Lotto.generateNumbers();
+ expect(numbers).toHaveLength(6);
+ expect(new Set(numbers).size).toBe(6);
+ });
+ });
+
+ describe("T-2-3 발행한 로또 수량 및 번호 출력 테스트", () => {
+ test("발행한 로또 수량과 번호가 콘솔에 출력되어야 한다", () => {
+ const consoleSpy = jest.spyOn(console, 'log');
+ const lottos = Lotto.generateMultipleLottos(3000);
+ Lotto.printLottos(lottos);
+ expect(consoleSpy).toHaveBeenCalledWith("3개를 구매했습니다.");
+ lottos.forEach(lotto => {
+ expect(consoleSpy).toHaveBeenCalledWith(`[${lotto.numbers.join(', ')}]`);
+ });
+ consoleSpy.mockRestore();
+ });
+ });
});
+
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000000..1f99952366
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,219 @@
+# 💲 로또 게임 💲
+
+
+
+
+
+---
+
+
+
+## 📢 3주차 학습 목표 📢
+
+1. [] `클래스 (객체)를 분리`하는 연습
+2. [] 도메인 로직에 대한 `단위 테스트`를 작성하는 연습
+3. [] `함수를 분리`하는 연습
+4. [] `함수별로 테스트`를 작성하는 연습
+
+---
+
+
+
+## 📖 사전 학습 목록 📖
+
+0. [☑️] `클래스와 객체`에 대한 학습
+ - S-0-1. [✅] 클래스와 객체의 개념
+ - S-0-2. [✅] 클래스와 객체를 분리하는 방법
+
+0. [☑️] `도메인 로직과 단위 테스트`에 대한 학습
+ - S-0-3. [✅] 도메인 로직의 개념
+ - S-0-4. [✅] 도메인 로직에 대한 단위 테스트를 작성하는 방법
+
+---
+
+
+
+## ✅ 기능 구현 목록 ✅
+
+1. [☑️] `로또 구입 금액 입력 받기`
+ - C-1-1. [✅] 금액 입력 메세지 출력
+ - ex) "구입금액을 입력해 주세요."
+ - C-1-2. [✅] 금액 입력 처리
+ - 금액은 1,000원 단위로 입력
+ - 공백이 들어갈 경우 제거
+ - ex) 8000
+ - C-1-3. [✅] 금액 입력 예외 처리
+ - 금액이 숫자가 아닌 경우
+ - 1,000원으로 나누어 떨어지지 않을 경우
+ - 금액이 1,000원 미만인 경우
+ - throw문을 사용해 "[ERROR]" 로 시작하는 메세지를 가지는 예외 발생
+ - ex) [ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다.
+
+2. [☑️] `로또 번호 발행`
+ - C-2-1. [✅] 구입 금액에 따른 로또 발행
+ - 1,000원 단위로 로또 번호 자동 생성
+ - C-2-2. [✅] 로또 번호 중복 없이 생성
+ - 숫자 범위 1~45
+ - C-2-3. [✅] 발행한 로또 수량 및 번호 출력
+ - 번호는 오름차순으로 정렬하여 출력
+ - ex) "8개를 구매했습니다."
+
+3. [] `당첨 번호 입력 받기`
+ - C-3-1. [] 당첨 번호 입력 메세지 출력
+ - ex) "당첨 번호를 입력해 주세요."
+ - C-3-2. [] 당첨 번호 입력 처리
+ - 번호는 쉼표로 구분하여 입력
+ - ex) 1,2,3,4,5,6
+ - C-3-3. [] 당첨 번호 입력 예외 처리
+ - 번호가 숫자가 아닌 경우
+ - 번호 범위가 1~45가 아닌 경우
+ - 번호가 중복된 경우
+ - 번호가 6개가 아닌 경우
+ - throw문을 사용해 "[ERROR]" 로 시작하는 메세지를 가지는 예외 발생
+ - ex) [ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
+
+4. [] `보너스 번호 입력 받기`
+ - C-4-1. [] 보너스 번호 입력 메세지 출력
+ - ex) "보너스 번호를 입력해 주세요."
+ - C-4-2. [] 보너스 번호 입력 처리
+ - 당첨 번호와 중복되지 않게 입력
+ - C-4-3. [] 보너스 번호 입력 예외 처리
+ - 번호가 숫자가 아닌 경우
+ - 번호 범위가 1~45가 아닌 경우
+ - 당첨 번호와 중복되는 경우
+ - throw문을 사용해 "[ERROR]" 로 시작하는 메세지를 가지는 예외 발생
+ - ex) [ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.
+
+5. [] `당첨 결과 계산`
+ - C-5-1. [] 사용자의 로또 번호와 당첨 번호 비교
+ - C-5-2. [] 당첨 내역 계산
+ - 등수별 당첨 기준에 따른 당첨 내역 확인
+ - C-5-3. [] 수익률 계산
+ - 수익률은 구입 금액 대비 당첨 금액으로 계산
+
+6. [] `당첨 결과 출력`
+ - C-6-1. [] 당첨 통계 메세지 출력
+ - ex) "당첨 통계"
+ - C-6-2. [] 당첨 내역 출력
+ - ex) "3개 일치 (5,000원) - 1개
+ - C-6-3. [] 수익 내역 출력
+ - 소수점 둘째 자리에서 반올림
+ - ex) "총 수익률은 62.5%입니다"
+
+---
+
+
+
+## 🔍 테스트 구현 목록 🔍
+
+T-1. [☑️] `로또 구입 금액 입력 받기 테스트`
+ - T-1-1. [✅] 금액 입력 메세지 출력 테스트
+ - T-1-2. [✅] 금액 입력 처리 테스트
+ - T-1-3. [✅] 금액 입력 예외 처리 테스트
+
+T-2. [☑️] `로또 번호 발행 테스트`
+ - T-2-1. [✅] 구입 금액에 따른 로또 발행 테스트
+ - T-2-2. [✅] 로또 번호 중복 없이 생성 테스트
+ - T-2-3. [✅] 발행한 로또 수량 및 번호 출력 테스트
+
+T-3. [] `당첨 번호 입력 받기 테스트`
+ - T-3-1. [✅] 당첨 번호 입력 메세지 출력 테스트
+ - T-3-2. [] 당첨 번호 입력 처리 테스트
+ - T-3-3. [] 당첨 번호 입력 예외 처리 테스트
+
+T-4. [] `보너스 번호 입력 받기 테스트`
+ - T-4-1. [] 보너스 번호 입력 메세지 출력 테스트
+ - T-4-2. [] 보너스 번호 입력 처리 테스트
+ - T-4-3. [] 보너스 번호 입력 예외 처리 테스트
+
+T-5. [] `당첨 결과 계산 테스트`
+ - T-5-1. [] 사용자의 로또 번호와 당첨 번호 비교 테스트
+ - T-5-2. [] 당첨 내역 계산 테스트
+ - T-5-3. [] 수익률 계산 테스트
+
+T-6. [] `당첨 결과 출력 테스트`
+ - T-6-1. [] 당첨 통계 메세지 출력 테스트
+ - T-6-2. [] 당첨 내역 출력 테스트
+ - T-6-3. [] 수익 내역 출력 테스트
+
+---
+
+
+
+## 🖋️ 요구사항 🖋️
+
+- `Node.js 18.17.1 버전`에서 실행 가능해야 한다.
+- 프로그램 실행의 `시작점은 App.js의 play 메서드`이다.
+- `package.json`을 변경할 수 없다.
+- `순수 Vanila JS`로만 구현한다.
+- `외부 라이브러리`(jQuery, Lodash 등)를 사용하지 않는다.
+- JavaScript `코드 컨벤션`을 지키면서 프로그래밍 한다
+- 프로그램 종료 시 `process.exit()`를 호출하지 않는다
+- 프로그램 구현이 완료되면 `ApplicationTest의 모든 테스트가 성공`해야 한다
+- 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 `수정하거나 이동하지 않는다`.
+- `indent(인덴트, 들여쓰기) 2`까지만 허용한다.
+ - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
+- `함수 (또는 메서드)가 한가지 일만` 하도록 최대한 작게 만든다.
+- jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 `테스트 코드로 확인`한다.
+- `@woowacourse/mission-utils`에서 제공하는 Random 및 Console API를 사용하여 구현한다
+- Random 값 추출은 `Random.pickNumberInRange()`를 활용한다.
+- 사용자의 값을 입력 받고 출력하기 위해서는 `Console.readLineAsync, Console.print`를 활용한다
+
+---
+
+
+
+## 🖋️ 추가된 요구사항 🖋️
+
+- `함수(또는 메서드)의 길이가 15라인`을 넘어가지 않도록 구현한다.
+- `함수(또는 메서드)가 한 가지 일만` 잘 하도록 구현한다.
+- `else를 지양`한다
+ - if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다
+- `도메인 로직에 단위 테스트`를 구현해야 한다.
+- 단 UI(Console.readLineAsync, Console.print) 로직에 대한 단위 테스트는 제외한다.
+ - 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
+
+---
+
+
+
+## 📢 2주차 공통 피드백 📢
+
+- `README. md`를 상세히 작성한다
+- 기능 목록을 `재검토`한다
+ - 에외 상황도 정리한다
+- 기능 목록을 `업데이트`한다
+- 값을 `하드 코딩`하지 않는다
+ - 상수를 만들고 이름을 부여해 변수의 역할이 무엇인지 의도를 드러낸다
+- `구현 순서`도 코딩 컨벤션이다
+ - 클래스는 필드, 생성자, 메서드 순으로 작성한다
+- `한 함수가 한 가지 기능`만 담당하게 한다
+- 함수가 한 가지 기능을 하는지 `확인하는 기준`을 세운다
+ - 함수의 길이를 15라인이 넘어가지 않도록 구현한다
+- JavaScript에서 `객체를 만드는 다양한 방법`을 이해하고 사용한다
+- `테스트를 작성하는 이유`에 대해 본인의 경험을 토대로 정리해본다
+- 처음부터 큰 단위의 테스트를 만들지 않는다
+
+---
+
+
+
+## 📢 1주차 공통 피드백 📢
+
+- `요구사항을 정확히 준수`한다
+- `커밋 메시지`를 의미 있게 작성한다
+- `git`을 통해 관리할 자원에 대해서도 고려한다
+- Pull Request를 보내기 전 `브랜치를 확인`한다
+- PR을 한 번 작성했다면 닫지 말고 `추가 커밋`을 한다
+- `이름을 통해 의도`를 드러낸다
+- `축약`하지 않는다
+- `공백`도 코딩 컨벤션이다
+- `공백 라인`을 의미 있게 사용한다
+- `space와 tab`을 혼용하지 않는다
+- `의미 없는 주석`을 달지 않는다
+- `linter`와 `Code Formatter`의 기능을 활용한다
+- `EOL(End Of Line)`
+- `불필요한 console.log`를 남기지 않는다
+- `JavaScript에서 제공하는 API`를 적극 활용한다
+
+---
diff --git a/docs/StudyLog.md b/docs/StudyLog.md
new file mode 100644
index 0000000000..0f6a3551fe
--- /dev/null
+++ b/docs/StudyLog.md
@@ -0,0 +1,178 @@
+# 📖 사전 학습 📖
+
+## 0. 클래스와 객체
+
+
+
+### S-0-1. 클래스와 객체의 개념
+
+ - `클래스`
+ - 객체를 생성하기 위한 틀
+ - ES6이후 도입
+ - constructor() 메소드를 포함하여 객체가 생성될때 필요한 초기 상태를 설정한다
+ - 클래스 안에는 객체의 행동을 정의하는 메소드들이 들어있다
+ - `객체`
+ - 클래스의 인스턴스
+ - 클래스를 기반으로 생성된 실체
+ - new 키워드를 사용하여 클래스의 인스턴스를 생성한다
+ - 해당 클래스에 정의된 속성과 메소드를 사용할 수 있다
+
+
+
+### S-0-2. 클래스와 객체를 분리하는 방법
+
+ - `클래스 분리`
+ - 클래스 정의를 별도의 파일로 만든다
+ - 필요할 때마다 import를 사용하여 불러온다
+ - 클래스를 분리하면 코드의 재사용성과 관리가 용이해진다
+ - 모듈화를 통해 프로젝트의 구조를 더욱 명확하게 할 수 있다
+ - 보통 소프트웨어의 설계 단계에서 고려된다
+ - 구조적인 측면에서 모듈화에 초점을 맞춘다
+ - `객체 분리`
+ - 특정 객체의 생성과 관리하는 로직을 별도의 함수나 모듈로 만드는 것을 의미한다
+ - 대규모 애플리케이션에서 객체의 생성과 관리를 캡슐화하고, 유지보수를 용이하게 한다
+ - 생성 로직의 캡슐화와 관리의 용이성에 초점을 맞춘다
+
+
+
+## 0. 도메인 로직과 단위 테스트
+
+
+
+### S-0-3. 도메인 로직의 개념
+
+ - `도메인 로직`
+ - 특정 비즈니스 영역에 대한 규칙, 계산, 절차 등을 의미
+ - 실제 비즈니스 문제를 해결하기 위한 소프트웨어 내부의 로직
+ - ex) 쇼핑몰의 주문을 처리하는 과정, 재고 관리, 할인 적용
+ - 시스템이 무엇을 할 것인지를 정의한다
+ - UI 로직이나 데이터베이스와 같은 인프라 로직과 구분된다
+ - `도메인 로직의 중요성`
+ - 소프트웨어의 핵심 기능을 구현한다
+ - 이를 통해 비즈니스 요구 사항을 만족시키고 사용자에게 가치를 제공한다
+ - 잘 설계된 도메인 로직은 시스템의 유지보수와 확장성을 크게 향상시킨다
+ - 비즈니스 규칙이 변경될 때 소프트웨어를 빠르고 쉽게 수정할 수 있도록 해준다
+ - 개발자는 비즈니스 규칙에 집중할 수 있고, UI나 데이터베이스 설계는 비즈니스 로직으로부터 독립적으로 진행할 수 있다
+
+
+
+### S-0-4. 도메인 로직에 대한 단위 테스트
+
+ - `함수 단위 테스트`
+ - 가장 작은 단위인 개별 함수 또는 메소드의 동작을 검증하는 테스트
+ - 해당 함수가 올바른 인자를 받고 예상대로 결과를 반환하는지, 적절한 예외를 발생시키는 지 등을 확인
+ - 주로 함수의 로직이 정확한지, 경계 조건과 에러처리가 적절한지에 중점
+ - 구현의 정확성에 집중
+ - 기술적인 측면에서의 검증
+ - `도메인 단위 테스트`
+ - 비즈니스 로직 또는 도메인 로직에 초점을 맞춘 테스트
+ - 개별 기능을 넘어서 비즈니스 요구 사항을 정확하게 충족하는 지 검증
+ - 비즈니스 프로세스의 흐름이나 상태 관리가 올바르게 이루어지는 지 테스트
+ - 시스템이 실제 사용 환경에서의 비즈니스 목표를 달성할 수 있는 지 확인
+ - 비즈니스 규칙과 요구 사항의 정확성에 집중
+ - 비즈니스 로직의 검증
+ - 종종 여러 함수가 통합되어 하나의 비즈니스 기능을 수행하는 것을 테스트한다
+ - 고립된 함수의 동작을 넘어선다
+
+
+
+---
+
+
+
+# 🖋️ 배운 내용 🖋️
+
+## 1. 로또 구입 금액 입력 받기
+
+
+
+### T-1-1. 금액 입력 메세지 출력 테스트
+
+ - `describe`
+ - 관련된 테스트 케이스들을 그룹화한다
+ - 이를 통해 테스트 코드를 더 관리하기 쉽고 구조화된 형태로 유지할 수 있다
+ - `테스트 케이스`
+ - test 또는 it 블록을 사용
+ - 개별 테스트 케이스를 정의한다
+ - 각 테스트 케이스는 독립적으로 실행되어야 한다
+ - 테스트하려는 한 가지 구체적인 동작 또는 사례를 검증해야 한다
+ - `모의 함수`
+ - Jest의 jest.fn() 또는 jest.spyOn()을 사용
+ - 함수를 모의한다
+ - 함수 호출을 추적하거나, 함수의 반환 값을 조작하거나, 특정 함수가 호출되었는지 여부를 검사하는 데 사용
+ - `Mock Module`
+ - jest.mock()을 사용한다
+ - 특정 모듈의 함수가 호출될 때 실제 구현 대신 테스트를 위한 구현을 사용하게 한다
+ - 외부 시스템과의 의존성을 제거하고 테스트를 독립적으로 만든다
+ - `비동기 테스트`
+ - async/await를 사용
+ - Jest는 비동기 코드가 완료될 때까지 기다린 후 테스트 결과를 확인할 수 있도록 지원한다
+ - `생명주기 메서드`
+ - beforeEach, beforeAll, afterEach, afterAll
+ - 테스트 전과 후에 반복적으로 실행되어야 하는 코드를 작성한다
+ - 예를 들어, beforeEach는 각 테스트가 시작하기 전에 실행되며, 테스트 환경을 초기화하는 데 유용하다
+ - `expect`
+ - 테스트에서 기대하는 조건을 명시한다
+ - Jest는 다양한 matcher를 통해 값이나 객체가 특정 조건을 만족하는지 검사한다
+
+
+
+### T-1-2. 금액 입력 처리 테스트
+
+ - `mockResolvedValue`
+ - 비동기 함수가 특정 값을 반환하도록 설정할 수 있다
+ - 이를 통해 Console.readLineAsync가 마치 사용자가 '8000'과 같은 유효한 금액을 입력한 것처럼 동작하게 할 수 있다
+
+
+
+### C-1-3. 금액 입력 예외 처리
+
+ - `정규 표현식`
+ - 문자열을 처리할 때 특정 패턴으로 문자열의 일부를 검색, 대체, 추출하는데 사용되는 형식 언어
+ - 복잡한 문자열 처리를 위해 사용되며, 간단한 메소드 호출로 강력한 문자열 검색 및 대체 작업을 수행할 수 있다
+
+ - `문자열 검증`
+ - 사용자 입력이 특정 형식을 따르는지 검사할 때 사용 (예: 이메일, 전화번호)
+
+ - `문자열 검색`
+ - 대량의 텍스트에서 패턴에 맞는 문자열을 찾을 때 사용
+
+ - `문자열 대체`
+ - 텍스트 내에서 특정 패턴을 찾아 다른 문자열로 대체할 때 사용
+
+ - `문자열 추출`
+ - 텍스트에서 특정 데이터를 추출할 때 사용
+
+ - `정규 표현식의 메소드`
+ - test()
+ - 주어진 문자열이 정규 표현식을 만족하는지 boolean으로 반환
+ - exec()
+ - 정규 표현식에 일치하는 문자열을 찾아 배열로 반환
+ - match()
+ - 문자열에 정규 표현식을 적용하여 일치하는 부분을 찾는다
+ - replace()
+ - 문자열에서 정규 표현식과 일치하는 부분을 다른 문자열로 대체
+
+ - `정규 표현식의 플래그`
+ - g (global)
+ - 전역 검색을 수행하며 문자열 내의 모든 일치 항목을 찾는다
+ - i (ignore case)
+ - 대소문자를 구분하지 않고 검색
+ - m (multiline)
+ - 여러 줄의 문자열에서 검색을 수행할 때 사용
+
+
+
+## 2. 로또 번호 발행
+
+
+
+### C-2-1. 구입 금액에 따른 로또 발행
+
+ - `Set`
+ - 중복을 허용하지 않는 데이터 집합을 만들 때 유용하다
+ - 로또 번호와 같이 고유한 값들의 집합을 생성할 때 Set을 사용하여 중복을 쉽게 제거할 수 있다
+
+ - `static`
+ - 클래스의 인스턴스 없이 호출할 수 있는 메소드를 생성할 수 있다
+ - 주로 유틸리티 함수나, 특정 인스턴스에 종속되지 않는 기능을 수행할 때 사용된다
diff --git a/src/App.js b/src/App.js
index c38b30d5b2..1a4898247e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,25 @@
+import InputManager from './InputManager.js';
+import Lotto from './Lotto.js';
+import { inputBonusNumber, inputWinningNumbers } from "./InputLotto";
+import { printResult, printProfit } from "./OutputLotto";
+
+
class App {
- async play() {}
+ async play() {
+ const inputManager = new InputManager();
+ const amount = await inputManager.enterAmount();
+ const lottos = Lotto.generateMultipleLottos(amount);
+
+ Lotto.printLottos(lottos);
+
+ await Lotto.inputWinningNumbers();
+
+ const lotto = await inputWinningNumbers();
+ const bonusNumber = await inputBonusNumber(lotto);
+ const matchedData = lottoChecker(randomNumbers, lotto, bonusNumber);
+ const winningData = printResult(matchedData);
+ printProfit(amount, winningData);
+ }
}
export default App;
diff --git a/src/InputAmount.js b/src/InputAmount.js
new file mode 100644
index 0000000000..e9d2577102
--- /dev/null
+++ b/src/InputAmount.js
@@ -0,0 +1,33 @@
+import { Console } from "@woowacourse/mission-utils";
+
+class InputManager {
+ async enterAmount() {
+ const inputAmount = await Console.readLineAsync("구입금액을 입력해 주세요.\n");
+ const cleanedAmount = this.cleanInput(inputAmount);
+ return this.validateAmount(cleanedAmount);
+ }
+
+ cleanInput(input) {
+ return input.replace(/\s+/g, '').replace(/,/g, '');
+ }
+
+ validateAmount(amount) {
+ const numericAmount = Number(amount);
+
+ if (isNaN(numericAmount)) {
+ throw new Error("[ERROR] 구입 금액은 숫자여야 합니다.");
+ }
+
+ if (numericAmount <= 0) {
+ throw new Error("[ERROR] 구입 금액은 양수여야 합니다.");
+ }
+
+ if (numericAmount % 1000 !== 0) {
+ throw new Error("[ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다.");
+ }
+
+ return numericAmount;
+ }
+}
+
+export default InputManager;
diff --git a/src/InputLotto.js b/src/InputLotto.js
new file mode 100644
index 0000000000..4a48066744
--- /dev/null
+++ b/src/InputLotto.js
@@ -0,0 +1,65 @@
+import { Console } from "@woowacourse/mission-utils";
+
+class InputLotto {
+ async inputWinningNumbers() {
+ const inputNumbers = await Console.readLineAsync("당첨 번호를 입력해 주세요.\n");
+ const winningNumbers = this.validateWinningNumbers(inputNumbers).split(',').map(Number);
+ return winningNumbers;
+ }
+
+ validateWinningNumbers(winningNumbers) {
+ if (!winningNumbers) {
+ throw new Error("[ERROR] 당첨 번호를 입력해 주세요.");
+ }
+
+ const commaNumbers = winningNumbers.split(',');
+ if (commaNumbers.length !== 6) {
+ throw new Error("[ERROR] 당첨 번호는 6개여야 합니다.");
+ }
+
+ if (new Set(commaNumbers).size !== 6) {
+ throw new Error("[ERROR] 당첨 번호에 중복이 있습니다.");
+ }
+
+ commaNumbers.forEach((number) => {
+ if (isNaN(Number(number))) {
+ throw new Error("[ERROR] 당첨 번호는 숫자여야 합니다.");
+ }
+
+ if (Number(number) < 1 || Number(number) > 45) {
+ throw new Error("[ERROR] 당첨 번호는 1부터 45 사이의 값이어야 합니다.");
+ }
+ });
+
+ return commaNumbers;
+ }
+
+ async inputBonusNumber(winningNumbers) {
+ while (true) {
+ try {
+ const bonusInput = await Console.readLineAsync("보너스 번호를 입력해 주세요.\n");
+ const bonusNumber = this.validateBonusNumber(winningNumbers, bonusInput);
+
+ return bonusNumber;
+ } catch (error) {
+ Console.print(error.message);
+ }
+ }
+ }
+
+ validateBonusNumber(winningNumbers, bonusInput) {
+ const bonusNumber = parseInt(bonusInput, 10);
+
+ if (isNaN(bonusNumber)) {
+ throw new Error("[ERROR] 보너스 번호는 숫자여야 합니다.");
+ } else if (bonusNumber < 1 || bonusNumber > 45) {
+ throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 값이어야 합니다.");
+ } else if (new Set([...winningNumbers, bonusNumber]).size !== 7) {
+ throw new Error("[ERROR] 당첨 번호와 중복되는 보너스 번호는 입력할 수 없습니다.");
+ }
+
+ return bonusNumber;
+ }
+}
+
+export default InputLotto;
\ No newline at end of file
diff --git a/src/Lotto.js b/src/Lotto.js
index cb0b1527e9..0d91f2f08e 100644
--- a/src/Lotto.js
+++ b/src/Lotto.js
@@ -1,18 +1,62 @@
+import { Random } from "@woowacourse/mission-utils"
+import { Console } from "@woowacourse/mission-utils"
+
class Lotto {
#numbers;
constructor(numbers) {
this.#validate(numbers);
- this.#numbers = numbers;
+ this.#numbers = numbers.sort((a, b) => a - b);
+ }
+
+ get numbers() {
+ return this.#numbers;
}
#validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}
+
+ if (new Set(numbers).size !== 6) {
+ throw new Error("[ERROR] 로또 번호에 중복이 있습니다.");
+ }
+
+ numbers.forEach((number) => {
+ if (isNaN(number)) {
+ throw new Error("[ERROR] 로또 번호는 숫자여야 합니다.");
+ }
+
+ if (number < 1 || number > 45) {
+ throw new Error("[ERROR] 로또 번호는 1부터 45 사이의 값이어야 합니다.");
+ }
+ });
}
- // TODO: 추가 기능 구현
+ get validate() {
+ return this.#validate;
+ }
+
+ static generateNumbers() {
+ const numbers = Random.pickUniqueNumbersInRange(1, 45, 6);
+ return numbers.sort((a, b) => a - b);
+ }
+
+ static generateMultipleLottos(amount) {
+ const numberOfLottos = Math.floor(amount / 1000);
+ const lottos = [];
+ for (let i = 0; i < numberOfLottos; i++) {
+ lottos.push(new Lotto(this.generateNumbers()));
+ }
+ return lottos;
+ }
+
+ static printLottos(lottos) {
+ Console.print(`${lottos.length}개를 구매했습니다.`);
+ lottos.forEach(lotto => {
+ Console.print(`[${lotto.numbers.join(', ')}]`);
+ });
+ }
}
export default Lotto;
diff --git a/src/OutputLotto.js b/src/OutputLotto.js
new file mode 100644
index 0000000000..4a48066744
--- /dev/null
+++ b/src/OutputLotto.js
@@ -0,0 +1,65 @@
+import { Console } from "@woowacourse/mission-utils";
+
+class InputLotto {
+ async inputWinningNumbers() {
+ const inputNumbers = await Console.readLineAsync("당첨 번호를 입력해 주세요.\n");
+ const winningNumbers = this.validateWinningNumbers(inputNumbers).split(',').map(Number);
+ return winningNumbers;
+ }
+
+ validateWinningNumbers(winningNumbers) {
+ if (!winningNumbers) {
+ throw new Error("[ERROR] 당첨 번호를 입력해 주세요.");
+ }
+
+ const commaNumbers = winningNumbers.split(',');
+ if (commaNumbers.length !== 6) {
+ throw new Error("[ERROR] 당첨 번호는 6개여야 합니다.");
+ }
+
+ if (new Set(commaNumbers).size !== 6) {
+ throw new Error("[ERROR] 당첨 번호에 중복이 있습니다.");
+ }
+
+ commaNumbers.forEach((number) => {
+ if (isNaN(Number(number))) {
+ throw new Error("[ERROR] 당첨 번호는 숫자여야 합니다.");
+ }
+
+ if (Number(number) < 1 || Number(number) > 45) {
+ throw new Error("[ERROR] 당첨 번호는 1부터 45 사이의 값이어야 합니다.");
+ }
+ });
+
+ return commaNumbers;
+ }
+
+ async inputBonusNumber(winningNumbers) {
+ while (true) {
+ try {
+ const bonusInput = await Console.readLineAsync("보너스 번호를 입력해 주세요.\n");
+ const bonusNumber = this.validateBonusNumber(winningNumbers, bonusInput);
+
+ return bonusNumber;
+ } catch (error) {
+ Console.print(error.message);
+ }
+ }
+ }
+
+ validateBonusNumber(winningNumbers, bonusInput) {
+ const bonusNumber = parseInt(bonusInput, 10);
+
+ if (isNaN(bonusNumber)) {
+ throw new Error("[ERROR] 보너스 번호는 숫자여야 합니다.");
+ } else if (bonusNumber < 1 || bonusNumber > 45) {
+ throw new Error("[ERROR] 보너스 번호는 1부터 45 사이의 값이어야 합니다.");
+ } else if (new Set([...winningNumbers, bonusNumber]).size !== 7) {
+ throw new Error("[ERROR] 당첨 번호와 중복되는 보너스 번호는 입력할 수 없습니다.");
+ }
+
+ return bonusNumber;
+ }
+}
+
+export default InputLotto;
\ No newline at end of file