-
Notifications
You must be signed in to change notification settings - Fork 232
[2단계 - 웹 기반 로또 게임] 레스 미션 제출합니다. #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
b4c7795
feat: 웹 진입점 및 mission-utils 브라우저 shim 추가
lee-eojin 35a0c28
style: CSS reset, 디자인 토큰, import 진입점 추가
lee-eojin 834442d
style: 전역 기본 스타일 및 타이포그래피 추가
lee-eojin 104ed4e
style: 로또 게임 및 결과 모달 레이아웃 스타일 추가
lee-eojin 470010e
docs: step2 기능 목록 및 프로젝트 구조 문서화
lee-eojin 187ca43
feat: priceInputForm 컴포넌트 추가
lee-eojin 2527268
feat: LottoList 컴포넌트 추가
lee-eojin 30ec47e
feat: WinningNumbersInputForm 컴포넌트 추가
lee-eojin ca44bb6
feat: GameResultDialog 컴포넌트 추가
lee-eojin 595f2d3
feat: WebApp 컨트롤러 추가
lee-eojin 96a49bb
style: 다이얼로그 가운데 정렬 및 min-height 적용
lee-eojin 3186d28
style: 보너스 번호 그룹 modifier 추가
lee-eojin 686fcec
style: 미사용 스타일 제거
lee-eojin 2bf11c5
refactor: 불필요한 defer 및 app id 제거
lee-eojin d46f7ad
feat: Enter 키 제출 지원을 위해서 form 태그로 변경
lee-eojin c54d3e9
style: 당첨번호 입력컨과 버튼 사이 간격 27px 추가
lee-eojin ff6d26b
chore: GitHub Pages 배포를 위한 homepage 설정
lee-eojin edcf8c5
docs: 배포 링크 추가 및 프로젝트 구조 업데이트
lee-eojin 12eee22
chore: 기능목록 점검
lee-eojin 44d7996
refactor: 입력 검증 책임을 뷰에서 컨트롤러로 이동
lee-eojin 04c983a
refactor: DOM 생성 헬퍼(createEl) 도입으로 선언적 구조로 변경
lee-eojin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,97 @@ | ||
| # 1단계 - 콘솔 기반 로또 게임 | ||
| # 2단계 - 웹 기반 로또 게임 | ||
|
|
||
| > 1단계에서 구현한 도메인 로직을 재사용하며, UI만 웹으로 전환한다. | ||
|
|
||
| ## 배포 링크 | ||
|
|
||
| https://lee-eojin.github.io/javascript-lotto | ||
|
|
||
| ## 구현할 기능 목록 | ||
|
|
||
| - [x] 로또 1장의 가격은 1,000원이다. | ||
| - [x] 로또 번호는 1부터 45 사이의 중복되지 않는 6개의 숫자로 구성된다. | ||
| - [x] 로또 번호는 오름차순으로 정렬하여 보여준다. | ||
| ### 구매 | ||
|
|
||
| - [x] 구입 금액을 입력하고 구입 버튼을 클릭하면 로또를 발행한다 | ||
| - [x] 구입한 로또 목록을 화면에 표시한다 | ||
| - [x] 유효하지 않은 금액 입력 시 에러 메시지를 표시한다 | ||
|
|
||
| ### 당첨 번호 입력 | ||
|
|
||
| - [x] 당첨 번호 6개와 보너스 번호 1개를 입력받는다 | ||
| - [x] 유효하지 않은 번호 입력 시 에러 메시지를 표시한다 | ||
|
|
||
| ### 결과 확인 | ||
|
|
||
| - [x] 결과 확인하기 버튼 클릭 시 당첨 통계 모달을 표시한다 | ||
| - [x] 등수별 당첨 개수와 수익률을 표시한다 | ||
|
|
||
| ### 재시작 | ||
|
|
||
| - [x] 다시 시작하기 버튼 클릭 시 게임을 초기화한다 | ||
|
|
||
| --- | ||
|
|
||
| ## 프로젝트 구조 | ||
|
|
||
| ``` | ||
| src/ | ||
| ├── step1-index.js # CLI 진입점 | ||
| ├── step2-index.js # 웹 진입점 (WebApp 초기화) | ||
| │ | ||
| ├── console/ # CLI 전용 (step1) | ||
| │ ├── ConsoleInputView.js | ||
| │ └── ConsoleOutputView.js | ||
| │ | ||
| ├── controller/ | ||
| │ ├── App.js # CLI 컨트롤러 (step1) | ||
| │ └── WebApp.js # 이벤트 핸들러, 도메인 - 뷰 연결 | ||
| │ | ||
| ├── web/ | ||
| │ └── components/ | ||
| │ ├── PriceInputForm.js # 구입 금액 입력 폼 | ||
| │ ├── LottoList.js # 구매한 로또 목록 표시 | ||
| │ ├── WinningNumbersInputForm.js # 당첨번호 + 보너스번호 입력 폼 | ||
| │ └── GameResultDialog.js # 당첨 통계 모달 | ||
| │ | ||
| ├── service/ | ||
| │ └── LottoManager.js | ||
| │ | ||
| ├── domain/ | ||
| │ ├── Lotto.js | ||
| │ ├── Money.js | ||
| │ ├── WinningNumber.js | ||
| │ ├── LottoResult.js | ||
| │ ├── Rank.js | ||
| │ └── generateLottos.js | ||
| │ | ||
| ├── constants/ | ||
| │ ├── messages.js | ||
| │ └── rules.js | ||
| │ | ||
| └── style/ | ||
| ├── main.css # @import 진입점 | ||
| ├── reset.css # 브라우저 기본값 초기화 (브라우저 호환성) | ||
| ├── variables.css # 디자인 토큰 - 색상, 폰트 (디자인 시안) | ||
| ├── base.css # 전역 태그 스타일, 타이포그래피 | ||
| └── layouts/ | ||
| ├── header.css | ||
| ├── footer.css | ||
| ├── lotto-game.css | ||
| └── lotto-result.css | ||
| ``` | ||
|
|
||
| - [x] 구입 금액을 입력하면 금액에 해당하는 만큼 로또를 발행한다. | ||
| - [x] 구입 금액이 1,000원 미만이면 구매할 수 없다. | ||
| #### CSS 파일 분리 기준 | ||
|
|
||
| - [x] 당첨 번호 6개와 보너스 번호 1개를 입력받는다. | ||
| - [x] 보너스 번호는 1부터 45 사이의 숫자여야 하며 당첨 번호와 중복될 수 없다. | ||
| > 파일을 나누는 기준: 변경 이유가 다른가 | ||
|
|
||
| - [x] 구매한 로또와 당첨 번호를 비교하여 일치 개수와 보너스 번호 일치 여부를 확인한다. | ||
| - [x] 일치 결과에 따라 등수를 판별한다. | ||
| - [x] 1등: 6개 번호 일치 / 2,000,000,000원 | ||
| - [x] 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 | ||
| - [x] 3등: 5개 번호 일치 / 1,500,000원 | ||
| - [x] 4등: 4개 번호 일치 / 50,000원 | ||
| - [x] 5등: 3개 번호 일치 / 5,000원 | ||
| - `reset.css` — 브라우저 호환성 이슈가 생길 때 변경 | ||
| - `variables.css` — 디자인 시안이 바뀔 때 변경 | ||
| - `base.css` — 전체 타이포그래피 정책이 바뀔 때 변경 | ||
| - `layouts/*.css` — 해당 레이아웃 디자인이 바뀔 때 변경 | ||
|
|
||
| - [x] 등수별 당첨 개수를 집계하고 구매 금액 대비 수익률을 계산하여 출력한다. | ||
| #### 컴포넌트 설계 기준 | ||
|
|
||
| - [x] 사용자가 잘못된 값을 입력하면 에러 메시지를 출력하고 해당 부분부터 다시 입력받는다. | ||
| - [x] 구입 금액, 당첨 번호, 보너스 번호, 재시작 여부(y/n)의 입력값을 검증한다. | ||
| > 컴포넌트는 자신의 DOM을 직접 생성하고 마운트/언마운트를 관리 | ||
|
|
||
| - [x] 당첨 통계를 출력한 뒤 재시작 여부를 입력받는다. | ||
| - [x] 재시작하면 구입 금액 입력부터 다시 시작하고, 종료하면 프로그램을 끝낸다. | ||
| - 각 컴포넌트는 `mount(container)` 로 필요할 때 DOM에 추가 | ||
| - 재시작 시 `unmount()` 로 DOM에서 제거 | ||
| - 도메인 로직은 컴포넌트가 아닌 `WebApp` → `LottoManager` 에서 처리 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import LottoManager from "../service/LottoManager.js"; | ||
| import PriceInputForm from "../web/components/PriceInputForm.js"; | ||
| import LottoList from "../web/components/LottoList.js"; | ||
| import WinningNumbersInputForm from "../web/components/WinningNumbersInputForm.js"; | ||
| import GameResultDialog from "../web/components/GameResultDialog.js"; | ||
|
|
||
| export default class WebApp { | ||
| #manager = new LottoManager(); | ||
| #priceForm; | ||
| #lottoList; | ||
| #winningForm; | ||
| #resultDialog; | ||
| #lottoListSection; | ||
| #winningSection; | ||
|
|
||
| constructor() { | ||
| this.#priceForm = new PriceInputForm({ onSubmit: this.#handlePurchaseSubmit.bind(this) }); | ||
| this.#resultDialog = new GameResultDialog({ onRestart: this.#handleRestart.bind(this) }); | ||
| this.#lottoListSection = document.querySelector('#lotto-list-section'); | ||
| this.#winningSection = document.querySelector('#winning-section'); | ||
|
|
||
| this.#priceForm.mount(document.querySelector('#purchase-section')); | ||
| this.#resultDialog.mount(document.body); | ||
| } | ||
|
|
||
| #handlePurchaseSubmit(value) { | ||
| try { | ||
| const lottos = this.#manager.buyLottos(Number(value)); | ||
| this.#priceForm.clearError(); | ||
|
|
||
| if (this.#lottoList) this.#lottoList.unmount(); | ||
| if (this.#winningForm) this.#winningForm.unmount(); | ||
|
|
||
| this.#lottoList = new LottoList(); | ||
| this.#lottoList.mount(this.#lottoListSection); | ||
| this.#lottoList.render(lottos); | ||
|
|
||
| this.#winningForm = new WinningNumbersInputForm({ | ||
| onSubmit: this.#handleWinningSubmit.bind(this), | ||
| }); | ||
| this.#winningForm.mount(this.#winningSection); | ||
| } catch (error) { | ||
| this.#priceForm.showError(error.message); | ||
| } | ||
| } | ||
|
|
||
| #handleWinningSubmit({ winningNumbers, bonusNumber }) { | ||
| try { | ||
| this.#validateNumbers([...winningNumbers, bonusNumber]); | ||
| const winningLotto = this.#manager.createWinningLotto(winningNumbers); | ||
| const winningNumber = this.#manager.createWinningNumber(winningLotto, bonusNumber); | ||
| const { prizeList, profitRate } = this.#manager.getLotteryResult(winningNumber); | ||
| this.#winningForm.clearError(); | ||
| this.#resultDialog.open(prizeList, profitRate); | ||
| } catch (error) { | ||
| this.#winningForm.showError(error.message); | ||
| } | ||
| } | ||
|
|
||
| #validateNumbers(numbers) { | ||
| if (numbers.some((n) => !Number.isInteger(n))) { | ||
| throw new Error("[ERROR] 로또 번호는 숫자여야 합니다."); | ||
| } | ||
| } | ||
|
|
||
| #handleRestart() { | ||
| this.#lottoList.unmount(); | ||
| this.#winningForm.unmount(); | ||
| this.#lottoList = null; | ||
| this.#winningForm = null; | ||
| this.#manager = new LottoManager(); | ||
| this.#priceForm.clearInput(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| body { | ||
| font-family: var(--font); | ||
| font-size: 15px; | ||
| font-weight: 400; | ||
| line-height: 24px; | ||
| letter-spacing: 0.5px; | ||
| background-color: var(--bg); | ||
| color: var(--black); | ||
| } | ||
|
|
||
| input { | ||
| border: 1px solid var(--gray-1); | ||
| border-radius: 4px; | ||
| outline: none; | ||
| font-size: 15px; | ||
| } | ||
|
|
||
| button { | ||
| border: none; | ||
| border-radius: 4px; | ||
| background-color: var(--primary); | ||
| color: var(--white); | ||
| font-weight: 700; | ||
| font-size: 14px; | ||
| line-height: 16px; | ||
| letter-spacing: 1.25px; | ||
| text-transform: uppercase; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| dialog { | ||
| border: none; | ||
| padding: 0; | ||
| border-radius: 4px; | ||
| } | ||
|
|
||
| dialog::backdrop { | ||
| background: var(--black-50); | ||
| } | ||
|
|
||
| /* typography */ | ||
| .title { | ||
| font-weight: 800; | ||
| font-size: 24px; | ||
| line-height: 1.5; | ||
| } | ||
|
|
||
| .subtitle { | ||
| font-weight: 600; | ||
| font-size: 20px; | ||
| line-height: 1.4; | ||
| } | ||
|
|
||
| .caption { | ||
| font-weight: 700; | ||
| font-size: 14px; | ||
| letter-spacing: 1.25px; | ||
| } | ||
|
|
||
| .error-message { | ||
| font-size: 13px; | ||
| color: #d32f2f; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| footer { | ||
| width: 100%; | ||
| padding: 28px 0 36px; | ||
| border-top: 1px solid var(--primary-20); | ||
| } | ||
|
|
||
| footer p { | ||
| text-align: center; | ||
| color: var(--primary); | ||
| line-height: 16px; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| header { | ||
| width: 100%; | ||
| height: 64px; | ||
| padding: 14px 130px; | ||
| background-color: var(--primary); | ||
| } | ||
|
|
||
| header .title { | ||
| color: var(--white); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[제안]
필드수가 좀 많아보이기는 한데, 하위 뷰 인스턴스랑 DOM 참조를 위한 변수들이라 크게 문제라고 생각되지는 않아요. WebView라는 중간 레이어를 둘까 하는 고민도 하셨다기에, 아마 이정도는 이 클래스에서 가져가도 괜찮은 크기의 책임이라고 판단하셔서 이렇게 넣어두신 것 같아요.
다만 이후 스펙이 커지면서 컴포넌트들이 많아지고, 컴포넌트간 의존성이 복잡해진다고 가정해봤을 때, 말씀해주신 WebView 레이어를 어떤식으로 구성해낼 수 있을지 구체적인 시나리오를 생각해보면 학습차원에서 좋을 것 같습니다👍👍
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
음 스펙이 커지면..
만약 어떤 영역에 하위 컴포너느가 추가되서 하나의 클래스가 너무 많은 것을 알게되는것 같다고 판단이 된다고 한다면, 제 코드상에서는 컴포넌트의 마운트/언마운트와 DOM참조 관리정도는 WebView가 담당하고, WebApp이 도메인호출이랑 비즈니스 흐름제어에만 집중하는 식도 괜찮을수있을것같다고 생각합니다.
직접 구현해보지는 못해서 확실하지는 않지만, WebApp이 하나의 의도만 전달하고 WebView가 어떤 컴포넌트를 어디에 마운트할지 결정하는 느낌으로 가도 괜찮을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네네 그런 방향도 충분히 괜찮다고 생각합니다. 사실 말씀주신 것처럼 실제 상황에서 직접 구현해보기 전까진 명확히 맞다 아니다를 판단할 순 없을거에요. 다만 이렇게 한번씩 미래의 상황을 가정해서 생각해보는 것과, 아예 고민하지 않고 지나가는 건 이후에 비슷한 상황을 맞닥뜨렸을 때 분명 차이가 있다고 생각되어서 한번 말씀드려보았습니다.