From 8b20f905d43b666ab0d6c4f90068dd0a86215574 Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 13:25:31 +0900 Subject: [PATCH 01/12] =?UTF-8?q?docs:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 328 ++++++++++++++++++++++++++++++++- src/constants/ErrorMesssage.js | 4 + src/constants/PromptMessage.js | 3 + src/{ => view}/view.js | 0 4 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 src/constants/ErrorMesssage.js create mode 100644 src/constants/PromptMessage.js rename src/{ => view}/view.js (100%) diff --git a/README.md b/README.md index b168a180..02ad6f29 100644 --- a/README.md +++ b/README.md @@ -1 +1,327 @@ -# javascript-planetlotto-precourse + \ No newline at end of file diff --git a/src/constants/ErrorMesssage.js b/src/constants/ErrorMesssage.js new file mode 100644 index 00000000..105f1d6c --- /dev/null +++ b/src/constants/ErrorMesssage.js @@ -0,0 +1,4 @@ +export const printErrorMessage = { + + +} \ No newline at end of file diff --git a/src/constants/PromptMessage.js b/src/constants/PromptMessage.js new file mode 100644 index 00000000..5e536390 --- /dev/null +++ b/src/constants/PromptMessage.js @@ -0,0 +1,3 @@ +export const PromptMessage = { + +} \ No newline at end of file diff --git a/src/view.js b/src/view/view.js similarity index 100% rename from src/view.js rename to src/view/view.js From dcf247f4103c8847a542922624fe8726b024dcb9 Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 14:44:19 +0900 Subject: [PATCH 02/12] =?UTF-8?q?docs:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=8F=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=20README.md=20=ED=8C=8C=EC=9D=BC=20=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 208 +++++++++++++++++++----------------------------------- 1 file changed, 74 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 02ad6f29..2be69fd4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ ## 목차 - [프로젝트 구조](#프로젝트-구조) -- [시퀀스 다이어그램](#시퀀스-다이어그램) - [기능 목록](#기능-목록) - [예외 처리](#예외-처리) - [실행 결과 예시](#실행-결과-예시) @@ -22,8 +21,7 @@ src/ ├── index.js # 실행 파일 ├── Lotto.js # 로또 모델 클래스 ├── views/ -│ ├── InputView.js # 입력 처리 뷰 -│ └── OutputView.js # 출력 처리 뷰 +│ └── view.js # 입,출력 처리 뷰 ├── controllers/ │ └── LottoController.js # 비즈니스 로직 컨트롤러 └── utils/ @@ -40,37 +38,34 @@ __tests__/ ``` -## 시퀀스 다이어그램 - -![시퀀스 다이어그램 (이미지)](docs/sequence-diagram.svg) ## 기능 목록 ### 1. 로또 구입 금액 입력 - 사용자로부터 로또 구입 금액을 입력받는다. -- 입력 형식: 정수 (예: `8000`, `14000`) -- 프롬프트: "구입금액을 입력해 주세요." +- 입력 형식: 500원 단위의 정수 (예: `8000`, `14000`) +- 프롬프트: "로또 구입금액을 입력해주세요. + >" ### 2. 로또 구입 금액 검증 - 빈 문자열 또는 공백만 입력한 경우 검증 - 정수 형식이어야 한다. -- 1,000원 단위로 나누어 떨어져야 한다. +- 500원 단위로 나누어 떨어져야 한다. - 양수여야 한다. **예외 상황:** -- 빈 문자열 또는 공백만 입력한 경우: `"[ERROR] 로또 구입 금액을 입력해 주세요."` -- 숫자가 아닌 경우: `"[ERROR] 로또 구입 금액은 숫자여야 합니다."` -- 소수점이 포함된 경우: `"[ERROR] 로또 구입 금액은 숫자여야 합니다."` -- 1,000원 단위가 아닌 경우: `"[ERROR] 로또 구입 금액은 1,000원 단위로 입력해야 합니다."` -- 양수가 아닌 경우: `"[ERROR] 로또 구입 금액은 양수여야 합니다."` - +- 로또의 구입금액을 500원 단위로 입력하지않았을때 : `"[ERROR] 로또의 구입금액은 500원 단위여야 합니다."` +- 로또 구입금액 숫자가 아닐때: `"[ERROR] 로또의 구입금액은 500원 단위 숫자여야 합니다."` +- 로또 구입금액 양수가 아닐때: `"[ERROR] 로또의 구입금액은 500원 단위 양수여야 합니다."` +- 로또 구입금액 정수가 아닐때: `"[ERROR] 로또의 구입금액은 500원 단위 정수여야 합니다.."` +- 로또 빈문자열 또는 공백만 입력헀을때: `"[ERROR] 로또의 구입금액은 500원 단위 금액으로 입력해야합니다."` ### 3. 로또 발행 -- 구입 금액에 따라 발행할 로또 수량을 계산한다. (구입 금액 / 1,000원) -- Random API를 사용하여 각 로또마다 중복되지 않는 6개의 숫자를 생성한다. (1~45 범위) +- 구입 금액에 따라 발행할 로또 수량을 계산한다. (구입 금액 / 500원) +- Random API를 사용하여 각 로또마다 중복되지 않는 5개의 숫자를 생성한다. (1~30 범위) - 생성된 로또 번호는 오름차순으로 정렬하여 저장한다. ### 4. 발행한 로또 출력 @@ -80,9 +75,9 @@ __tests__/ - 출력 형식: ``` - 8개를 구매했습니다. - [8, 21, 23, 41, 42, 43] - [3, 5, 11, 16, 32, 38] + 2개를 구매했습니다. + [8, 11, 13, 21, 22] + [1, 3, 6, 14, 22] ... ``` @@ -90,46 +85,42 @@ __tests__/ ### 5. 당첨 번호 입력 -- 당첨 번호 6개를 쉼표로 구분하여 입력받는다. -- 입력 형식: 쉼표로 구분된 정수 (예: `1,2,3,4,5,6`) -- 프롬프트: "당첨 번호를 입력해 주세요." +- 당첨 번호 5개를 쉼표로 구분하여 입력받는다. +- 입력 형식: 쉼표로 구분된 정수 (예: `1,2,3,4,5`) +- 프롬프트: " 로또 당첨 번호를 5번호 를 구분하여 입력해주세요. + (번호 구분은 , 를통해서 입력하시오) + * 로또의 당첨 번호는 중복될 수 없습니다. + * 로또의 당첨 번호는 음수, 소수가 될수 없습니다. + >" ### 6. 당첨 번호 검증 - 빈 문자열 또는 공백만 입력한 경우 검증 -- 정확히 6개의 정수여야 한다. -- 각 숫자가 1 이상 45 이하여야 한다. +- 정확히 5개의 정수여야 한다. +- 각 숫자가 1 이상 30 이하여야 한다. - 중복된 숫자가 없어야 한다. -**예외 상황:** - -- 빈 문자열 또는 공백만 입력한 경우: `"[ERROR] 당첨 번호를 입력해 주세요."` -- 숫자가 아닌 값이 포함된 경우: `"[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."` -- 소수점이 포함된 경우: `"[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."` -- 빈 값이 포함된 경우 (예: "1,2,,4,5,6"): `"[ERROR] 로또 번호는 6개여야 합니다."` -- 6개가 아닌 경우: `"[ERROR] 로또 번호는 6개여야 합니다."` -- 1~45 범위를 벗어난 경우: `"[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."` -- 중복이 있는 경우: `"[ERROR] 로또 번호에 중복된 숫자가 있습니다."` - ### 7. 보너스 번호 입력 - 보너스 번호 1개를 입력받는다. - 입력 형식: 정수 (예: `7`) -- 프롬프트: "보너스 번호를 입력해 주세요." +- 프롬프트: "보너스 번호를 입력해주세요. + * 보너스 번호는 당첨번호와 중복될 수 없습니다. + >" ### 8. 보너스 번호 검증 - 빈 문자열 또는 공백만 입력한 경우 검증 - 정수 형식이어야 한다. -- 1 이상 45 이하여야 한다. +- 1 이상 30 이하여야 한다. - 당첨 번호와 중복되지 않아야 한다. **예외 상황:** - 빈 문자열 또는 공백만 입력한 경우: `"[ERROR] 보너스 번호를 입력해 주세요."` -- 숫자가 아닌 경우: `"[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."` -- 소수점이 포함된 경우: `"[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."` -- 1~45 범위를 벗어난 경우: `"[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."` +- 숫자가 아닌 경우: `"[ERROR] 보너스 번호는 1부터 30 사이의 숫자여야 합니다."` +- 소수점이 포함된 경우: `"[ERROR] 보너스 번호는 1부터 30 사이의 숫자여야 합니다."` +- 1~45 범위를 벗어난 경우: `"[ERROR] 보너스 번호는 1부터 30 사이의 숫자여야 합니다."` - 당첨 번호와 중복되는 경우: `"[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."` ### 9. 당첨 결과 계산 @@ -137,12 +128,12 @@ __tests__/ - 각 로또 번호와 당첨 번호를 비교하여 일치하는 개수를 계산한다. - 보너스 번호 포함 여부를 확인한다. - 등수를 판정한다. - - **1등**: 6개 일치 → 2,000,000,000원 - - **2등**: 5개 일치 + 보너스 일치 → 30,000,000원 - - **3등**: 5개 일치 → 1,500,000원 - - **4등**: 4개 일치 → 50,000원 - - **5등**: 3개 일치 → 5,000원 - - **꽝**: 2개 이하 일치 → 0원 + **1등**: 5개 번호 일치 / 100,000,000원 + **2등**: 4개 번호 + 보너스 번호 일치 / 10,000,000원 + **3등**: 4개 번호 일치 / 1,500,000원 + **4등**: 3개 번호 일치 + 보너스 번호 일치 / 500,000원 + **5등**: 2개 번호 일치 + 보너스 번호 일치 / 5,000원 + **꽝**: 2개 번호 일치 이하 / 0원 ### 10. 당첨 통계 계산 @@ -164,12 +155,12 @@ __tests__/ ``` 당첨 통계 --- - 3개 일치 (5,000원) - 1개 - 4개 일치 (50,000원) - 0개 - 5개 일치 (1,500,000원) - 0개 - 5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 - 6개 일치 (2,000,000,000원) - 0개 - 총 수익률은 62.5%입니다. + 5개 일치 (100,000,000원) - 0개 + 4개 일치, 보너스 번호 일치 (10,000,000원) - 0개 + 4개 일치 (1,500,000원) - 0개 + 3개 일치, 보너스 번호 일치 (500,000원) - 0개 + 2개 일치, 보너스 번호 일치 (5,000원) - 1개 + 2개 일치 이하, (0원) - 1개 ``` @@ -185,29 +176,29 @@ __tests__/ 구입금액을 입력해 주세요. 8000 8개를 구매했습니다. -[8, 21, 23, 41, 42, 43] -[3, 5, 11, 16, 32, 38] -[7, 11, 16, 35, 36, 44] -[1, 8, 11, 31, 41, 42] -[13, 14, 16, 38, 42, 45] -[7, 11, 30, 40, 42, 43] -[2, 13, 22, 32, 38, 45] -[1, 3, 5, 14, 22, 45] +[8, 21, 23, 41, 42] +[3, 5, 11, 16, 32] +[7, 11, 16, 35, 36] +[1, 8, 11, 31, 41] +[13, 14, 16, 38, 42] +[7, 11, 30, 40, 42] +[2, 13, 22, 32, 38] +[1, 3, 5, 14, 22] 당첨 번호를 입력해 주세요. -1,2,3,4,5,6 +1,2,3,4,5 보너스 번호를 입력해 주세요. -7 +6 당첨 통계 --- -3개 일치 (5,000원) - 1개 -4개 일치 (50,000원) - 0개 -5개 일치 (1,500,000원) - 0개 -5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 -6개 일치 (2,000,000,000원) - 0개 -총 수익률은 62.5%입니다. +5개 일치 (100,000,000원) - 0개 +4개 일치, 보너스 번호 일치 (10,000,000원) - 0개 +4개 일치 (1,500,000원) - 0개 +3개 일치, 보너스 번호 일치 (500,000원) - 0개 +2개 일치, 보너스 번호 일치 (5,000원) - 1개 +0개 일치 (0원) - 1개 ``` @@ -222,28 +213,28 @@ __tests__/ 1. **빈 문자열 또는 공백만 입력한 경우** - 메시지: `"[ERROR] 로또 구입 금액을 입력해 주세요."` 2. **숫자가 아닌 경우** - - 메시지: `"[ERROR] 로또 구입 금액은 숫자여야 합니다."` + - 메시지: `"[ERROR] 로또 구입 금액은 500원 단위 숫자여야 합니다."` 3. **소수점이 포함된 경우** - - 메시지: `"[ERROR] 로또 구입 금액은 숫자여야 합니다."` -4. **1,000원 단위가 아닌 경우** - - 메시지: `"[ERROR] 로또 구입 금액은 1,000원 단위로 입력해야 합니다."` + - 메시지: `"[ERROR] 로또 구입 금액은 500원 단위 정수여야 합니다."` +4. **500원 단위가 아닌 경우** + - 메시지: `"[ERROR] 로또 구입 금액은 500원 단위로 입력해야 합니다."` 5. **양수가 아닌 경우** - - 메시지: `"[ERROR] 로또 구입 금액은 양수여야 합니다."` + - 메시지: `"[ERROR] 로또 구입 금액은 500원 단위 양수여야 합니다."` ### 당첨 번호 1. **빈 문자열 또는 공백만 입력한 경우** - 메시지: `"[ERROR] 당첨 번호를 입력해 주세요."` 2. **숫자가 아닌 값이 포함된 경우** - - 메시지: `"[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."` + - 메시지: `"[ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다."` 3. **소수점이 포함된 경우** - - 메시지: `"[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."` -4. **빈 값이 포함된 경우 (예: "1,2,,4,5,6")** - - 메시지: `"[ERROR] 로또 번호는 6개여야 합니다."` + - 메시지: `"[ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다."` +4. **빈 값이 포함된 경우 (예: "1,2,,4,5")** + - 메시지: `"[ERROR] 로또 번호는 5개여야 합니다."` 5. **6개가 아닌 경우** - - 메시지: `"[ERROR] 로또 번호는 6개여야 합니다."` + - 메시지: `"[ERROR] 로또 번호는 5개여야 합니다."` 6. **1~45 범위를 벗어난 경우** - - 메시지: `"[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."` + - 메시지: `"[ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다."` 7. **중복이 있는 경우** - 메시지: `"[ERROR] 로또 번호에 중복된 숫자가 있습니다."` @@ -252,11 +243,11 @@ __tests__/ 1. **빈 문자열 또는 공백만 입력한 경우** - 메시지: `"[ERROR] 보너스 번호를 입력해 주세요."` 2. **숫자가 아닌 경우** - - 메시지: `"[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."` + - 메시지: `"[ERROR] 보너스 번호는 1부터 30 사이의 정수여야 합니다."` 3. **소수점이 포함된 경우** - - 메시지: `"[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."` -4. **1~45 범위를 벗어난 경우** - - 메시지: `"[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."` + - 메시지: `"[ERROR] 보너스 번호는 1부터 30 사이의 정수여야 합니다."` +4. **1~30 범위를 벗어난 경우** + - 메시지: `"[ERROR] 보너스 번호는 1부터 30 사이의 정수여야 합니다."` 5. **당첨 번호와 중복되는 경우** - 메시지: `"[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."` @@ -273,55 +264,4 @@ __tests__/ ``` -## 도전과 회고 - -### 이번 주 도전한 것들 - -- MVP 아키텍처 적용: Model-View-Presenter로 역할 분리 강화 (지난주 MVC 대비 Presenter 책임 명확화) -- SOLID 원칙 준수: 단일 책임을 중심으로 클래스/메서드 분리, 하드코딩 상수 추출 -- 유틸 클래스 정적화: 상태가 없는 유틸(Validator/Generator/Calculator/View)을 `static` 메서드로 구성해 불필요한 인스턴스 생성 제거 -- 입력 파싱·검증 일원화: Controller에 있던 파싱을 `InputValidator`로 통합해 관심사 분리 및 재사용성 향상 -- 예외 케이스 확장: 빈 문자열/공백, 소수점, 비정수 입력 등 경계·오류 입력까지 테스트로 명세화 -- 테스트 우선 개발: 제공된 통합 테스트를 기준으로, 단위 테스트를 확장해 점진적으로 기능 완성 -- 커밋 컨벤션 적용: 기능 단위 커밋과 Angular 스타일 메시지로 변경 이력 명확화 - -### 시도와 실패, 그리고 개선 - -- 초기 파싱 위치 결정 실패: Controller에서 파싱을 처리해 테스트 중복과 책임 혼재 발생 → 파싱을 `InputValidator`로 이동하여 테스트 단순화 및 응집도 상승 -- README 중복 서술: "입출력 형식"과 예시의 중복으로 가독성 저하 → 형식은 기능 항목에 반영하고, 전체 흐름은 "실행 결과 예시"로만 유지 -- ApplicationTest 선실행 실패: App 통합 전 실행으로 실패 → Lotto/Validator/Generator/Calculator 단위 완성 후 View/Controller 순으로 통합하여 해결 -- 에러 메시지 일관성: 스펙과 다른 문구가 섞일 위험 → 모든 예외 메시지를 스펙 기반으로 통일하고 테스트로 고정 - -### 배운 점 - -- 파싱과 검증을 한곳으로 모으면(Validator) Controller는 흐름 제어에 집중할 수 있어 유지보수가 쉬워진다. -- 유틸성 로직은 `static`으로 단순화하면 인스턴스 수명/상태 고민이 줄고 테스트가 쉬워진다. -- 테스트는 명세 역할을 하므로, 경계/에러 케이스를 먼저 고정하면 리팩터링 내성이 커진다. - -### 아쉬웠던 점 - -- 입력 파이프라인의 국제화(i18n) 메시지 분리 -- 등수/금액 상수의 별도 도메인 모듈화 및 포맷터 분리(통화/퍼센트) -- 테스트 파라미터화로 중복 축소(`describe.each`, `test.each` 적극 활용) - -### 이번 주 차별점 - -- Validator 단일 진실의 원천(SSOT): 입력 파싱+검증을 한곳에 모아 Controller는 흐름 제어만 수행 -- 재입력 루프 UX: 에러 발생 지점부터 입력 재요청하는 루프 구조 확립 -- 방어적 파싱 전략: 빈 값/공백/소수/비정수/문자 입력까지 명시적으로 처리 -- 정적 유틸리티 일관 적용: Validator/Generator/Calculator/View 전부 상태 없는 static 메서드로 단순화 -- 예외 메시지 표준화: 스펙 문구로 통일하고 테스트로 고정(회귀 방지) -- 계약 기반 테스트 정렬: 통합 테스트(ApplicationTest)를 계약으로 두고 단위 테스트를 그에 맞춰 확장 -- 도메인 상수화: LOTTO_PRICE, MIN/MAX, COUNT를 코드 상수로 관리하여 하드코딩 제거 -- 제약 주도 리팩토링: 들여쓰기 ≤ 2, 함수 길이 ≤ 15라인을 설계 제약으로 삼아 구조 개선 -- 문서 중복 제거: 입출력 형식 중복을 제거하고 기능 중심으로 재편 -- 작업 흐름 개선: 기능 단위 커밋, 브랜치 upstream 설정 및 PR 생성 흐름 정착 -- 포맷팅 정책 테스트 고정: 수익률(둘째 자리 반올림)·금액 표기 형식을 테스트로 보장 -- 컨트롤러 무상태화 확인: 상태 공유 없이 입력→계산→출력의 흐름만 조합 -- 테스트 파라미터화 강화: 등수 판정/통계 계산에 `test.each` 적용 여지 문서화 - -``` - -``` - \ No newline at end of file From d7259d21dd75e4150c7e290244527d0a09393084 Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 15:13:07 +0900 Subject: [PATCH 03/12] =?UTF-8?q?docs:=20README.md=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=20=EC=9D=B4=EC=8A=A4=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=ED=94=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2be69fd4..b14b815b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ src/ ├── App.js # 프로그램 진입점 ├── index.js # 실행 파일 -├── Lotto.js # 로또 모델 클래스 +├── model/ +│ └── Lotto.js # 로또 모델 클래스 ├── views/ │ └── view.js # 입,출력 처리 뷰 ├── controllers/ @@ -87,11 +88,11 @@ __tests__/ - 당첨 번호 5개를 쉼표로 구분하여 입력받는다. - 입력 형식: 쉼표로 구분된 정수 (예: `1,2,3,4,5`) -- 프롬프트: " 로또 당첨 번호를 5번호 를 구분하여 입력해주세요. - (번호 구분은 , 를통해서 입력하시오) - * 로또의 당첨 번호는 중복될 수 없습니다. - * 로또의 당첨 번호는 음수, 소수가 될수 없습니다. - >" +- 프롬프트: `" 로또 당첨 번호를 5번호 를 구분하여 입력해주세요. + (번호 구분은 , 를통해서 입력하시오). + * 로또의 당첨 번호는 중복될 수 없습니다. + * 로또의 당첨 번호는 음수, 소수가 될수 없습니다. + >"` ### 6. 당첨 번호 검증 @@ -104,9 +105,9 @@ __tests__/ - 보너스 번호 1개를 입력받는다. - 입력 형식: 정수 (예: `7`) -- 프롬프트: "보너스 번호를 입력해주세요. - * 보너스 번호는 당첨번호와 중복될 수 없습니다. - >" +- 프롬프트: "보너스 번호를 입력해주세요. + * 보너스 번호는 당첨번호와 중복될 수 없습니다. + >` ### 8. 보너스 번호 검증 @@ -128,12 +129,12 @@ __tests__/ - 각 로또 번호와 당첨 번호를 비교하여 일치하는 개수를 계산한다. - 보너스 번호 포함 여부를 확인한다. - 등수를 판정한다. - **1등**: 5개 번호 일치 / 100,000,000원 - **2등**: 4개 번호 + 보너스 번호 일치 / 10,000,000원 - **3등**: 4개 번호 일치 / 1,500,000원 - **4등**: 3개 번호 일치 + 보너스 번호 일치 / 500,000원 - **5등**: 2개 번호 일치 + 보너스 번호 일치 / 5,000원 - **꽝**: 2개 번호 일치 이하 / 0원 + - **1등**: 5개 번호 일치 / 100,000,000원 + - **2등**: 4개 번호 + 보너스 번호 일치 / 10,000,000원 + - **3등**: 4개 번호 일치 / 1,500,000원 + - **4등**: 3개 번호 일치 + 보너스 번호 일치 / 500,000원 + - **5등**: 2개 번호 일치 + 보너스 번호 일치 / 5,000원 + - **꽝**: 2개 번호 일치 이하 / 0원 ### 10. 당첨 통계 계산 From e7109c205fdae82491bef936813590098ddb023d Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 15:32:51 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20ErrorMessage=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/ErrorMesssage.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/constants/ErrorMesssage.js b/src/constants/ErrorMesssage.js index 105f1d6c..adce5f67 100644 --- a/src/constants/ErrorMesssage.js +++ b/src/constants/ErrorMesssage.js @@ -1,4 +1,21 @@ -export const printErrorMessage = { - +export const ErrorMessages = { + PURCHSE_AOUNT: { + EMPTY: '[ERROR] 로또 구입 금액을 입력해 주세요.', + NOT_NUMBER: '[ERROR] 로또의 구입금액은 500원 단위 숫자여야 합니다.', + NOT_DIVISIBLE: '[ERROR] 로또의 구입금액은 500원 단위여야 합니다.', + NOT_POSITIVE: "[ERROR] 로또의 구입금액은 500원 단위 양수여야 합니다.", + }, -} \ No newline at end of file + WINNING_NUMBERS: { + EMPTY: '[ERROR] 로또 당첨번호를 입력해 주세요.', + INVALID_COUNT: '[ERROR] 로또 번호는 5개여야 합니다.', + INVALID_RANGE: '[ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다.', + DUPLICATE: '[ERROR] 로또 당첨 번호에 중복된 숫자가 있습니다.', + }, + + BONUS_NUMBER: { + EMPTY: '[ERROR] 보너스 번호를 입력해 주세요.', + INVALID_RANGE: '[ERROR] 보너스 번호는 1부터 30 사이의 정수여야 합니다.', + DUPLICATE: '[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.' + }, +}; From 65b6681e9e62c23d551408a51d218be039cfb61e Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 16:35:01 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20Lotto=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/LottoConstants.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/constants/LottoConstants.js diff --git a/src/constants/LottoConstants.js b/src/constants/LottoConstants.js new file mode 100644 index 00000000..03f389ff --- /dev/null +++ b/src/constants/LottoConstants.js @@ -0,0 +1,9 @@ +class LottoConstants { + static LOTTO_PRICE = 500; + static LOTTO_NUMBER_COUNT = 5; + static MIN_NUMBER = 1; + static MAX_NUMBER = 30; +} + +export default LottoConstants; + From 9a1c288eb232f658d13996e3bfcd3cec38c71d2c Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 16:35:53 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20prize=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/PrizeConstants.js | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/constants/PrizeConstants.js diff --git a/src/constants/PrizeConstants.js b/src/constants/PrizeConstants.js new file mode 100644 index 00000000..a0445b3f --- /dev/null +++ b/src/constants/PrizeConstants.js @@ -0,0 +1,36 @@ +class PrizeConstants { + static RANK = { + FIRST: 1, + SECOND: 2, + THIRD: 3, + FOURTH: 4, + FIFTH: 5, + NONE: 0, + }; + + static MATCH_COUNT = { + FIRST: 5, + SECOND: 4, + THIRD: 4, + FOURTH: 3, + FIFTH: 2, + }; + + static PRIZE_AMOUNTS = { + 1: 100000000, + 2: 10000000, + 3: 1500000, + 4: 500000, + 5: 5000, + }; + + static PROFIT_RATE_MULTIPLIER = 100; + static PROFIT_RATE_DECIMAL_PLACES = 1; +} + +export default PrizeConstants; + + + + + From e4a0c265d574ca4ae45292c5b9b48d2d627ffa53 Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 16:36:44 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20prompt=20message=20=EC=83=81?= =?UTF-8?q?=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/PromptMessage.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/constants/PromptMessage.js b/src/constants/PromptMessage.js index 5e536390..29079581 100644 --- a/src/constants/PromptMessage.js +++ b/src/constants/PromptMessage.js @@ -1,3 +1,5 @@ -export const PromptMessage = { - -} \ No newline at end of file +export const PromptMessages = { + PURCHASE_AMOUNT: "구입금액을 입력해 주세요.\n", + WINNING_NUMBERS: "\n당첨 번호를 입력해 주세요.\n", + BONUS_NUMBER: "\n보너스 번호를 입력해 주세요.\n" +}; From cf0b50361b721a78655a618a816c050205db098b Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 16:38:53 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20contoroller=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/LottoController.js | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/controller/LottoController.js diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 00000000..4ddd0912 --- /dev/null +++ b/src/controller/LottoController.js @@ -0,0 +1,85 @@ +import InputView from "../view/view.js"; +import OutputView from "../view/view.js"; +import InputValidator from "../utils/InputValidator.js"; +import PrizeCalculator from "../utils/PrizeCalculator.js"; +import LottoConstants from "../constants/LottoConstants.js"; +import LottoGenerator from "../utils/Lottogenerator.js"; +import view from "../view/view.js"; + +class LottoController { + async run() { + const lottos = await this.purchaseLottos(); + OutputView.OutputView.printPurchasedLottos(lottos); + + const winningNumbers = await this.getWinningNumbers(); + const bonusNumber = await this.getBonusNumber(winningNumbers); + + this.calculateResults(lottos, winningNumbers, bonusNumber); + } + + async purchaseLottos() { + while (true) { + try { + const input = await InputView.InputView.askAmount(); + InputValidator.validatePurchaseAmount(input); + + const purchaseAmount = Number(input); + const lottoCount = this.calculateLottoCount(purchaseAmount); + return LottoGenerator.generateLottos(lottoCount); + } catch (error) { + view.OutputView.printErrorMessage(error.message); + } + } + } + + calculateLottoCount(purchaseAmount) { + return purchaseAmount / LottoConstants.LOTTO_PRICE; + } + + async getWinningNumbers() { + while (true) { + try { + const input = await InputView.readWinningNumbers(); + return InputValidator.validateWinningNumbers(input); + } catch (error) { + view.OutputView.printErrorMessage(message); + } + } + } + + async getBonusNumber(winningNumbers) { + while (true) { + try { + const input = await InputView.readBonusNumber(); + return InputValidator.validateBonusNumber(input, winningNumbers); + } catch (error) { + view.OutputView.printErrorMessage(message); + } + } + } + + calculateResults(lottos, winningNumbers, bonusNumber) { + const statistics = PrizeCalculator.calculateStatistics( + lottos, + winningNumbers, + bonusNumber + ); + this.printResults(statistics, lottos.length); + } + + printResults(statistics, lottoCount) { + OutputView.printStatisticsHeader(); + OutputView.printPrizeStatistics(statistics); + + const totalPrize = PrizeCalculator.calculateTotalPrizeAmount(statistics); + const purchaseAmount = lottoCount * LottoConstants.LOTTO_PRICE; + const profitRate = PrizeCalculator.calculateProfitRate( + totalPrize, + purchaseAmount + ); + + OutputView.printResult(profitRate); + } +} + +export default LottoController; \ No newline at end of file From 6f50433ddfe73ef4bb1692428765bc4508f809a3 Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 16:39:27 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20model=20Lotto.js=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Lotto.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/model/Lotto.js diff --git a/src/model/Lotto.js b/src/model/Lotto.js new file mode 100644 index 00000000..87bfbf87 --- /dev/null +++ b/src/model/Lotto.js @@ -0,0 +1,57 @@ +import LottoConstants from "../constants/LottoConstants.js"; +import ErrorMessage from "../constants/ErrorMesssage.js"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = [...numbers].sort((a, b) => a - b); + } + + #validate(numbers) { + this.#validateCount(numbers); + this.#validateDuplicate(numbers); + this.#validateRange(numbers); + } + + #validateCount(numbers) { + if (numbers.length !== LottoConstants.LOTTO_NUMBER_COUNT) { + throw new Error(ErrorMessage.WINNING_NUMBERS_INVALID_COUNT); + } + } + + #validateDuplicate(numbers) { + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== numbers.length) { + throw new Error(ErrorMessage.WINNING_NUMBERS_DUPLICATE); + } + } + + #validateRange(numbers) { + const outOfRange = numbers.some( + (number) => + number < LottoConstants.MIN_NUMBER || + number > LottoConstants.MAX_NUMBER + ); + if (outOfRange) { + throw new Error(ErrorMessage.WINNING_NUMBERS_INVALID_RANGE); + } + } + + getNumbers() { + return [...this.#numbers]; + } + + countMatchingNumbers(winningNumbers) { + return this.#numbers.filter((number) => + winningNumbers.includes(number) + ).length; + } + + hasBonusNumber(bonusNumber) { + return this.#numbers.includes(bonusNumber); + } +} + +export default Lotto; \ No newline at end of file From 693913ba4dc7b6237732ecb7a2215d7e7e0acab6 Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 16:41:10 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20Utill=20class=20InputValidator,?= =?UTF-8?q?=20LottoGenerator,=20PrizeCalculator=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/InputValidator.js | 142 +++++++++++++++++++++++++++++++++++ src/utils/LottoGenerator.js | 24 ++++++ src/utils/PrizeCalculator.js | 77 +++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 src/utils/InputValidator.js create mode 100644 src/utils/LottoGenerator.js create mode 100644 src/utils/PrizeCalculator.js diff --git a/src/utils/InputValidator.js b/src/utils/InputValidator.js new file mode 100644 index 00000000..e57675ee --- /dev/null +++ b/src/utils/InputValidator.js @@ -0,0 +1,142 @@ +import LottoConstants from "../constants/LottoConstants.js"; +import ErrorMessage from "../constants/ErrorMesssage.js"; + +class InputValidator { + + static validatePurchaseAmount(amount) { + this.validateAmountType(amount); + this.validateAmountDivisible(amount); + this.validateAmountPositive(amount); + } + + static validateAmountType(amount) { + const trimmedAmount = String(amount).trim(); + if (trimmedAmount === "") { + throw new Error(ErrorMessage.PURCHASE_AMOUNT_EMPTY); + } + + const amountNumber = Number(trimmedAmount); + if (isNaN(amountNumber)) { + throw new Error(ErrorMessage.PURCHASE_AMOUNT_NOT_NUMBER); + } + + if (!Number.isInteger(amountNumber)) { + throw new Error(ErrorMessage.PURCHASE_AMOUNT_NOT_NUMBER); + } + } + + static validateAmountDivisible(amount) { + const amountNumber = Number(amount); + if (amountNumber % LottoConstants.LOTTO_PRICE !== 0) { + throw new Error(ErrorMessage.PURCHASE_AMOUNT_NOT_DIVISIBLE); + } + } + + static validateAmountPositive(amount) { + const amountNumber = Number(amount); + if (amountNumber <= 0) { + throw new Error(ErrorMessage.PURCHASE_AMOUNT_NOT_POSITIVE); + } + } + + static validateWinningNumbers(input) { + const numbers = this.parseWinningNumbers(input); + this.validateNumberCount(numbers); + this.validateNumberRange(numbers); + this.validateUniqueNumbers(numbers); + return numbers; + } + + static parseWinningNumbers(input) { + if (!input || input.trim() === "") { + throw new Error(ErrorMessage.WINNING_NUMBERS_EMPTY); + } + + const numbers = input.split(",").map((number) => { + const trimmed = number.trim(); + if (trimmed === "") { + throw new Error(ErrorMessage.WINNING_NUMBERS_INVALID_COUNT); + } + + const parsed = Number(trimmed); + if (isNaN(parsed)) { + throw new Error(ErrorMessage.WINNING_NUMBERS_INVALID_RANGE); + } + + if (!Number.isInteger(parsed)) { + throw new Error(ErrorMessage.WINNING_NUMBERS_INVALID_RANGE); + } + + return parsed; + }); + + return numbers; + } + + static validateNumberCount(numbers) { + if (numbers.length !== LottoConstants.LOTTO_NUMBER_COUNT) { + throw new Error(ErrorMessage.WINNING_NUMBERS_INVALID_COUNT); + } + } + + static validateNumberRange(numbers) { + const outOfRange = numbers.some( + (number) => + number < LottoConstants.MIN_NUMBER || + number > LottoConstants.MAX_NUMBER + ); + if (outOfRange) { + throw new Error(ErrorMessage.WINNING_NUMBERS_INVALID_RANGE); + } + } + + static validateUniqueNumbers(numbers) { + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== numbers.length) { + throw new Error(ErrorMessage.WINNING_NUMBERS_DUPLICATE); + } + } + + static validateBonusNumber(input, winningNumbers) { + const bonusNumber = this.parseBonusNumber(input); + this.validateBonusRange(bonusNumber); + this.validateBonusDuplicate(bonusNumber, winningNumbers); + return bonusNumber; + } + + static parseBonusNumber(input) { + const trimmedInput = input.trim(); + + if (trimmedInput === "") { + throw new Error(ErrorMessage.BONUS_NUMBER_EMPTY); + } + + const bonusNumber = Number(trimmedInput); + if (isNaN(bonusNumber)) { + throw new Error(ErrorMessage.BONUS_NUMBER_INVALID_RANGE); + } + + if (!Number.isInteger(bonusNumber)) { + throw new Error(ErrorMessage.BONUS_NUMBER_INVALID_RANGE); + } + + return bonusNumber; + } + + static validateBonusRange(bonusNumber) { + if ( + bonusNumber < LottoConstants.MIN_NUMBER || + bonusNumber > LottoConstants.MAX_NUMBER + ) { + throw new Error(ErrorMessage.BONUS_NUMBER_INVALID_RANGE); + } + } + + static validateBonusDuplicate(bonusNumber, winningNumbers) { + if (winningNumbers.includes(bonusNumber)) { + throw new Error(ErrorMessage.BONUS_NUMBER_DUPLICATE); + } + } +} + +export default InputValidator; \ No newline at end of file diff --git a/src/utils/LottoGenerator.js b/src/utils/LottoGenerator.js new file mode 100644 index 00000000..a46b57c9 --- /dev/null +++ b/src/utils/LottoGenerator.js @@ -0,0 +1,24 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import Lotto from "../model/Lotto.js"; +import LottoConstants from "../constants/LottoConstants.js"; + +class LottoGenerator { + static generateLottoNumbers() { + const numbers = MissionUtils.Random.pickUniqueNumbersInRange( + LottoConstants.MIN_NUMBER, + LottoConstants.MAX_NUMBER, + LottoConstants.LOTTO_NUMBER_COUNT + ); + return new Lotto(numbers); + } + + static generateLottos(count) { + const lottos = []; + for (let i = 0; i < count; i++) { + lottos.push(this.generateLottoNumbers()); + } + return lottos; + } +} + +export default LottoGenerator; \ No newline at end of file diff --git a/src/utils/PrizeCalculator.js b/src/utils/PrizeCalculator.js new file mode 100644 index 00000000..3c27f744 --- /dev/null +++ b/src/utils/PrizeCalculator.js @@ -0,0 +1,77 @@ +import PrizeConstants from "../constants/PrizeConstants.js"; + +class PrizeCalculator { + static determineRank(matchCount, hasBonus) { + if (matchCount === PrizeConstants.MATCH_COUNT.FIRST) { + return PrizeConstants.RANK.FIRST; + } + if ( + matchCount === PrizeConstants.MATCH_COUNT.SECOND && + hasBonus + ) { + return PrizeConstants.RANK.SECOND; + } + if (matchCount === PrizeConstants.MATCH_COUNT.THIRD) { + return PrizeConstants.RANK.THIRD; + } + if (matchCount === PrizeConstants.MATCH_COUNT.FOURTH && + hasBonus + ) { + return PrizeConstants.RANK.FOURTH; + } + if (matchCount === PrizeConstants.MATCH_COUNT.FIFTH && + hasBonus + ) { + return PrizeConstants.RANK.FIFTH; + } + return PrizeConstants.RANK.NONE; + } + + static calculatePrizeAmount(rank) { + if (rank === PrizeConstants.RANK.NONE) { + return 0; + } + return PrizeConstants.PRIZE_AMOUNTS[rank]; + } + + static calculateStatistics(lottos, winningNumbers, bonusNumber) { + const statistics = { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + }; + + lottos.forEach((lotto) => { + const matchCount = lotto.countMatchingNumbers(winningNumbers); + const hasBonus = lotto.hasBonusNumber(bonusNumber); + const rank = PrizeCalculator.determineRank(matchCount, hasBonus); + + if (rank !== PrizeConstants.RANK.NONE) { + statistics[rank]++; + } + }); + + return statistics; + } + + static calculateTotalPrizeAmount(statistics) { + let totalAmount = 0; + Object.keys(statistics).forEach((rank) => { + const count = statistics[rank]; + const prizeAmount = PrizeCalculator.calculatePrizeAmount(Number(rank)); + totalAmount += count * prizeAmount; + }); + return totalAmount; + } + + static calculateProfitRate(totalPrize, purchaseAmount) { + const profitRate = + (totalPrize / purchaseAmount) * + PrizeConstants.PROFIT_RATE_MULTIPLIER; + return Number(profitRate.toFixed(PrizeConstants.PROFIT_RATE_DECIMAL_PLACES)); + } +} + +export default PrizeCalculator; \ No newline at end of file From 18bd6041f89c33ee4f959fcd24e220c782d0e2ce Mon Sep 17 00:00:00 2001 From: USER Date: Sat, 10 Jan 2026 16:41:46 +0900 Subject: [PATCH 11/12] =?UTF-8?q?docs:=20=EB=AC=B8=EC=84=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=8B=A4=ED=96=89=20=EC=9A=94=EC=86=8C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/App.js | 7 ++++++- src/constants/ErrorMesssage.js | 26 ++++++++++++++------------ src/view/view.js | 2 +- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b14b815b..5ba2ce4d 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ __tests__/ - 로또의 구입금액을 500원 단위로 입력하지않았을때 : `"[ERROR] 로또의 구입금액은 500원 단위여야 합니다."` - 로또 구입금액 숫자가 아닐때: `"[ERROR] 로또의 구입금액은 500원 단위 숫자여야 합니다."` - 로또 구입금액 양수가 아닐때: `"[ERROR] 로또의 구입금액은 500원 단위 양수여야 합니다."` -- 로또 구입금액 정수가 아닐때: `"[ERROR] 로또의 구입금액은 500원 단위 정수여야 합니다.."` +- 로또 구입금액 정수가 아닐때: `"[ERROR] 로또의 구입금액은 500원 단위 정수여야 합니다."` - 로또 빈문자열 또는 공백만 입력헀을때: `"[ERROR] 로또의 구입금액은 500원 단위 금액으로 입력해야합니다."` ### 3. 로또 발행 @@ -88,7 +88,7 @@ __tests__/ - 당첨 번호 5개를 쉼표로 구분하여 입력받는다. - 입력 형식: 쉼표로 구분된 정수 (예: `1,2,3,4,5`) -- 프롬프트: `" 로또 당첨 번호를 5번호 를 구분하여 입력해주세요. +- 프롬프트: `로또 당첨 번호를 5번호 를 구분하여 입력해주세요. (번호 구분은 , 를통해서 입력하시오). * 로또의 당첨 번호는 중복될 수 없습니다. * 로또의 당첨 번호는 음수, 소수가 될수 없습니다. diff --git a/src/App.js b/src/App.js index 091aa0a5..13626625 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import LottoController from "./controller/LottoController.js"; + class App { - async run() {} + async run() { + const controller = new LottoController; + await controller.run(); + } } export default App; diff --git a/src/constants/ErrorMesssage.js b/src/constants/ErrorMesssage.js index adce5f67..2bd50938 100644 --- a/src/constants/ErrorMesssage.js +++ b/src/constants/ErrorMesssage.js @@ -1,21 +1,23 @@ -export const ErrorMessages = { +const ErrorMessage = { PURCHSE_AOUNT: { - EMPTY: '[ERROR] 로또 구입 금액을 입력해 주세요.', - NOT_NUMBER: '[ERROR] 로또의 구입금액은 500원 단위 숫자여야 합니다.', - NOT_DIVISIBLE: '[ERROR] 로또의 구입금액은 500원 단위여야 합니다.', - NOT_POSITIVE: "[ERROR] 로또의 구입금액은 500원 단위 양수여야 합니다.", + EMPTY: '로또 구입 금액을 입력해 주세요.', + NOT_NUMBER: '로또의 구입금액은 500원 단위 숫자여야 합니다.', + NOT_DIVISIBLE: '로또의 구입금액은 500원 단위여야 합니다.', + NOT_POSITIVE: "로또의 구입금액은 500원 단위 양수여야 합니다.", }, WINNING_NUMBERS: { - EMPTY: '[ERROR] 로또 당첨번호를 입력해 주세요.', - INVALID_COUNT: '[ERROR] 로또 번호는 5개여야 합니다.', - INVALID_RANGE: '[ERROR] 로또 번호는 1부터 30 사이의 숫자여야 합니다.', - DUPLICATE: '[ERROR] 로또 당첨 번호에 중복된 숫자가 있습니다.', + EMPTY: '로또 당첨번호를 입력해 주세요.', + INVALID_COUNT: '로또 번호는 5개여야 합니다.', + INVALID_RANGE: '로또 번호는 1부터 30 사이의 숫자여야 합니다.', + DUPLICATE: '로또 당첨 번호에 중복된 숫자가 있습니다.', }, BONUS_NUMBER: { - EMPTY: '[ERROR] 보너스 번호를 입력해 주세요.', - INVALID_RANGE: '[ERROR] 보너스 번호는 1부터 30 사이의 정수여야 합니다.', - DUPLICATE: '[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.' + EMPTY: '보너스 번호를 입력해 주세요.', + INVALID_RANGE: '보너스 번호는 1부터 30 사이의 정수여야 합니다.', + DUPLICATE: '보너스 번호는 당첨 번호와 중복될 수 없습니다.' }, }; + +export default ErrorMessage; diff --git a/src/view/view.js b/src/view/view.js index ae6afd9c..3ae31edd 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -87,4 +87,4 @@ const OutputView = { }, }; -export { InputView, OutputView }; +export default { InputView, OutputView }; From c9a8bc2cae543b7b9bcc9754ee341edde1d897b2 Mon Sep 17 00:00:00 2001 From: USER Date: Mon, 9 Feb 2026 15:44:47 +0900 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20view=EB=A5=BC=20=ED=86=B5=ED=95=9C?= =?UTF-8?q?=20lotto=20system=20=EC=9E=AC=EC=A0=95=EB=B0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/ErrorMesssage.js | 28 ++++++-------- src/controller/LottoController.js | 63 +++++++++++++++++++------------ src/view/view.js | 1 - 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/constants/ErrorMesssage.js b/src/constants/ErrorMesssage.js index 2bd50938..7f449922 100644 --- a/src/constants/ErrorMesssage.js +++ b/src/constants/ErrorMesssage.js @@ -1,23 +1,17 @@ const ErrorMessage = { - PURCHSE_AOUNT: { - EMPTY: '로또 구입 금액을 입력해 주세요.', - NOT_NUMBER: '로또의 구입금액은 500원 단위 숫자여야 합니다.', - NOT_DIVISIBLE: '로또의 구입금액은 500원 단위여야 합니다.', - NOT_POSITIVE: "로또의 구입금액은 500원 단위 양수여야 합니다.", - }, + PURCHASE_AMOUNT_EMPTY: '로또 구입 금액을 입력해 주세요.', + PURCHASE_AMOUNT_NOT_NUMBER: '로또의 구입금액은 500원 단위 숫자여야 합니다.', + PURCHASE_AMOUNT_NOT_DIVISIBLE: '로또의 구입금액은 500원 단위여야 합니다.', + PURCHASE_AMOUNT_NOT_POSITIVE: "로또의 구입금액은 500원 단위 양수여야 합니다.", - WINNING_NUMBERS: { - EMPTY: '로또 당첨번호를 입력해 주세요.', - INVALID_COUNT: '로또 번호는 5개여야 합니다.', - INVALID_RANGE: '로또 번호는 1부터 30 사이의 숫자여야 합니다.', - DUPLICATE: '로또 당첨 번호에 중복된 숫자가 있습니다.', - }, + WINNING_NUMBERS_EMPTY: '로또 당첨번호를 입력해 주세요.', + WINNING_NUMBERS_INVALID_COUNT: '로또 번호는 5개여야 합니다.', + WINNING_NUMBERS_INVALID_RANGE: '로또 번호는 1부터 30 사이의 숫자여야 합니다.', + WINNING_NUMBERS_DUPLICATE: '로또 당첨 번호에 중복된 숫자가 있습니다.', - BONUS_NUMBER: { - EMPTY: '보너스 번호를 입력해 주세요.', - INVALID_RANGE: '보너스 번호는 1부터 30 사이의 정수여야 합니다.', - DUPLICATE: '보너스 번호는 당첨 번호와 중복될 수 없습니다.' - }, + BONUS_NUMBER_EMPTY: '보너스 번호를 입력해 주세요.', + BONUS_NUMBER_INVALID_RANGE: '보너스 번호는 1부터 30 사이의 정수여야 합니다.', + BONUS_NUMBER_DUPLICATE: '보너스 번호는 당첨 번호와 중복될 수 없습니다.', }; export default ErrorMessage; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js index 4ddd0912..7ebf3668 100644 --- a/src/controller/LottoController.js +++ b/src/controller/LottoController.js @@ -1,15 +1,16 @@ -import InputView from "../view/view.js"; -import OutputView from "../view/view.js"; +import view from "../view/view.js"; import InputValidator from "../utils/InputValidator.js"; import PrizeCalculator from "../utils/PrizeCalculator.js"; import LottoConstants from "../constants/LottoConstants.js"; -import LottoGenerator from "../utils/Lottogenerator.js"; -import view from "../view/view.js"; +import LottoGenerator from "../utils/LottoGenerator.js"; +import PrizeConstants from "../constants/PrizeConstants.js"; + +const { InputView, OutputView } = view; class LottoController { async run() { const lottos = await this.purchaseLottos(); - OutputView.OutputView.printPurchasedLottos(lottos); + this.printLottos(lottos); const winningNumbers = await this.getWinningNumbers(); const bonusNumber = await this.getBonusNumber(winningNumbers); @@ -17,17 +18,21 @@ class LottoController { this.calculateResults(lottos, winningNumbers, bonusNumber); } + printLottos(lottos) { + const lottoNumbers = lottos.map(lotto => lotto.getNumbers()); + OutputView.printPurchasedLottos(lottoNumbers); + } + async purchaseLottos() { while (true) { try { - const input = await InputView.InputView.askAmount(); - InputValidator.validatePurchaseAmount(input); + const amount = await InputView.askAmount(); + InputValidator.validatePurchaseAmount(amount); - const purchaseAmount = Number(input); - const lottoCount = this.calculateLottoCount(purchaseAmount); + const lottoCount = this.calculateLottoCount(amount); return LottoGenerator.generateLottos(lottoCount); } catch (error) { - view.OutputView.printErrorMessage(error.message); + OutputView.printErrorMessage(error.message); } } } @@ -39,10 +44,14 @@ class LottoController { async getWinningNumbers() { while (true) { try { - const input = await InputView.readWinningNumbers(); - return InputValidator.validateWinningNumbers(input); + const numbers = await InputView.askWinningLotto(); + // InputValidator의 개별 검증 메서드 사용 + InputValidator.validateNumberCount(numbers); + InputValidator.validateNumberRange(numbers); + InputValidator.validateUniqueNumbers(numbers); + return numbers; } catch (error) { - view.OutputView.printErrorMessage(message); + OutputView.printErrorMessage(error.message); } } } @@ -50,10 +59,12 @@ class LottoController { async getBonusNumber(winningNumbers) { while (true) { try { - const input = await InputView.readBonusNumber(); - return InputValidator.validateBonusNumber(input, winningNumbers); + const bonusNumber = await InputView.askBonusNumber(); + InputValidator.validateBonusRange(bonusNumber); + InputValidator.validateBonusDuplicate(bonusNumber, winningNumbers); + return bonusNumber; } catch (error) { - view.OutputView.printErrorMessage(message); + OutputView.printErrorMessage(error.message); } } } @@ -68,17 +79,19 @@ class LottoController { } printResults(statistics, lottoCount) { - OutputView.printStatisticsHeader(); - OutputView.printPrizeStatistics(statistics); + // Map 형식으로 변환 (0~5 등급) + const countsByRank = new Map(); + const totalWinning = (statistics[1] || 0) + (statistics[2] || 0) + (statistics[3] || 0) + (statistics[4] || 0) + (statistics[5] || 0); + const noMatchCount = lottoCount - totalWinning; - const totalPrize = PrizeCalculator.calculateTotalPrizeAmount(statistics); - const purchaseAmount = lottoCount * LottoConstants.LOTTO_PRICE; - const profitRate = PrizeCalculator.calculateProfitRate( - totalPrize, - purchaseAmount - ); + countsByRank.set(0, noMatchCount); + countsByRank.set(1, statistics[1] || 0); + countsByRank.set(2, statistics[2] || 0); + countsByRank.set(3, statistics[3] || 0); + countsByRank.set(4, statistics[4] || 0); + countsByRank.set(5, statistics[5] || 0); - OutputView.printResult(profitRate); + OutputView.printResult(countsByRank); } } diff --git a/src/view/view.js b/src/view/view.js index 3ae31edd..78398b17 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -67,7 +67,6 @@ const OutputView = { const output = [ '당첨 통계', - '---', `5개 일치 (100,000,000원) - ${getCount(1)}개`, `4개 일치, 보너스 번호 일치 (10,000,000원) - ${getCount(2)}개`, `4개 일치 (1,500,000원) - ${getCount(3)}개`,