Skip to content
Merged
Show file tree
Hide file tree
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 Mar 11, 2026
35a0c28
style: CSS reset, 디자인 토큰, import 진입점 추가
lee-eojin Mar 11, 2026
834442d
style: 전역 기본 스타일 및 타이포그래피 추가
lee-eojin Mar 11, 2026
104ed4e
style: 로또 게임 및 결과 모달 레이아웃 스타일 추가
lee-eojin Mar 11, 2026
470010e
docs: step2 기능 목록 및 프로젝트 구조 문서화
lee-eojin Mar 11, 2026
187ca43
feat: priceInputForm 컴포넌트 추가
lee-eojin Mar 16, 2026
2527268
feat: LottoList 컴포넌트 추가
lee-eojin Mar 16, 2026
30ec47e
feat: WinningNumbersInputForm 컴포넌트 추가
lee-eojin Mar 16, 2026
ca44bb6
feat: GameResultDialog 컴포넌트 추가
lee-eojin Mar 16, 2026
595f2d3
feat: WebApp 컨트롤러 추가
lee-eojin Mar 16, 2026
96a49bb
style: 다이얼로그 가운데 정렬 및 min-height 적용
lee-eojin Mar 16, 2026
3186d28
style: 보너스 번호 그룹 modifier 추가
lee-eojin Mar 16, 2026
686fcec
style: 미사용 스타일 제거
lee-eojin Mar 16, 2026
2bf11c5
refactor: 불필요한 defer 및 app id 제거
lee-eojin Mar 16, 2026
d46f7ad
feat: Enter 키 제출 지원을 위해서 form 태그로 변경
lee-eojin Mar 16, 2026
c54d3e9
style: 당첨번호 입력컨과 버튼 사이 간격 27px 추가
lee-eojin Mar 16, 2026
ff6d26b
chore: GitHub Pages 배포를 위한 homepage 설정
lee-eojin Mar 16, 2026
edcf8c5
docs: 배포 링크 추가 및 프로젝트 구조 업데이트
lee-eojin Mar 16, 2026
12eee22
chore: 기능목록 점검
lee-eojin Mar 16, 2026
44d7996
refactor: 입력 검증 책임을 뷰에서 컨트롤러로 이동
lee-eojin Mar 20, 2026
04c983a
refactor: DOM 생성 헬퍼(createEl) 도입으로 선언적 구조로 변경
lee-eojin Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 88 additions & 20 deletions README.md
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` 에서 처리
26 changes: 21 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,31 @@
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>🎱 행운의 로또</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<title>🎱 행운의 로또</title>
<link rel="stylesheet" href="./src/style/main.css" />
<script src="https://cdn.jsdelivr.net/npm/@woowacourse/mission-utils@1.0.1/dist/mission-utils.min.js"></script>
<script type="module" src="./src/step2-index.js"></script>
</head>

<body>
<div id="app">
<h1>🎱 행운의 로또</h1>
<div>
<header>
<h1 class="title">🎱 행운의 로또</h1>
</header>

<main>
<section class="lotto-card">
<h2 class="lotto-card__title title">🎱 내 번호 당첨 확인 🎱</h2>
<div id="purchase-section"></div>
<div id="lotto-list-section"></div>
<div id="winning-section"></div>
</section>
</main>

<footer>
<p class="caption">Copyright 2023. woowacourse</p>
</footer>
</div>
<script type="module" src="./src/step2-index.js"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"start-step2": "vite",
"deploy": "vite build --base=/javascript-lotto/ --minify=false && npx gh-pages -d dist"
},
"homepage": "https://{username}.github.io/javascript-lotto",
"homepage": "https://lee-eojin.github.io/javascript-lotto",
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
Expand Down
8 changes: 8 additions & 0 deletions src/controller/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,22 @@ export default class App {

async #readWinningLotto() {
const numbers = await InputView.readWinningNumbers();
this.#validateNumbers(numbers);
return this.#lottoManager.createWinningLotto(numbers);
}

async #getWinningNumber(winningLotto) {
const bonus = await InputView.readBonusNumber();
this.#validateNumbers([bonus]);
return this.#lottoManager.createWinningNumber(winningLotto, bonus);
}

#validateNumbers(numbers) {
if (numbers.some((n) => !Number.isInteger(n))) {
throw new Error("[ERROR] 로또 번호는 숫자여야 합니다.");
}
}

async #processResult(winningLotto) {
const { prizeList, profitRate } = this.#lottoManager.getLotteryResult(winningLotto);
OutputView.printStatistics({ prizeList, profitRate });
Expand Down
74 changes: 74 additions & 0 deletions src/controller/WebApp.js
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;
Comment on lines +8 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]
필드수가 좀 많아보이기는 한데, 하위 뷰 인스턴스랑 DOM 참조를 위한 변수들이라 크게 문제라고 생각되지는 않아요. WebView라는 중간 레이어를 둘까 하는 고민도 하셨다기에, 아마 이정도는 이 클래스에서 가져가도 괜찮은 크기의 책임이라고 판단하셔서 이렇게 넣어두신 것 같아요.
다만 이후 스펙이 커지면서 컴포넌트들이 많아지고, 컴포넌트간 의존성이 복잡해진다고 가정해봤을 때, 말씀해주신 WebView 레이어를 어떤식으로 구성해낼 수 있을지 구체적인 시나리오를 생각해보면 학습차원에서 좋을 것 같습니다👍👍

Copy link
Copy Markdown
Author

@lee-eojin lee-eojin Mar 20, 2026

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가 어떤 컴포넌트를 어디에 마운트할지 결정하는 느낌으로 가도 괜찮을까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 그런 방향도 충분히 괜찮다고 생각합니다. 사실 말씀주신 것처럼 실제 상황에서 직접 구현해보기 전까진 명확히 맞다 아니다를 판단할 순 없을거에요. 다만 이렇게 한번씩 미래의 상황을 가정해서 생각해보는 것과, 아예 고민하지 않고 지나가는 건 이후에 비슷한 상황을 맞닥뜨렸을 때 분명 차이가 있다고 생각되어서 한번 말씀드려보았습니다.


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();
}
}
3 changes: 3 additions & 0 deletions src/step2-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
* step 2의 시작점이 되는 파일입니다.
* 노드 환경에서 사용하는 readline 등을 불러올 경우 정상적으로 빌드할 수 없습니다.
*/
import WebApp from "./controller/WebApp.js";

new WebApp();
63 changes: 63 additions & 0 deletions src/style/base.css
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;
}
11 changes: 11 additions & 0 deletions src/style/layouts/footer.css
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;
}
10 changes: 10 additions & 0 deletions src/style/layouts/header.css
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);
}
Loading