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 @@ +# 💲 로또 게임 💲 + +![image](https://github.com/woowacourse-precourse/javascript-lotto-6/assets/146679818/1a60f055-13bd-41b7-89ab-7fa061216f70) + +
+ +--- + +
+ +## 📢 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