Skip to content
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

[2단계 - 웹 기반 로또 게임] 해리(최현웅) 미션 제출합니다. #314

Merged
merged 26 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c8a7683
docs(REQUEST-STEP2.md): 기능 요구 사항 작성
hwinkr Feb 26, 2024
df38d02
docs(REQUEST-STEP2.md): 기능 요구 사항 작성
hwinkr Feb 26, 2024
a24d1ed
chore: .gitignore에 dist 폴더 삭제
hwinkr Feb 26, 2024
292553f
feat(index.html): 웹 기반 로또 게임 html 레이아웃 작성
hwinkr Mar 2, 2024
577e6d0
config(packaga.json): build 명령어 추가
hwinkr Mar 2, 2024
b59c89f
feat(step2-index.js): 웹 기반 로또 게임 엔트리 구현
hwinkr Mar 2, 2024
fc0233c
chore(index.html): id 이름 변경
hwinkr Mar 2, 2024
2916c87
chore(constants): 웹 기반 로또 게임에서 사용할 상수 객체 설정
hwinkr Mar 2, 2024
c6d5447
chore(index.html): id 이름 변경
hwinkr Mar 2, 2024
13a644f
chore(constants): 웹 기반 로또 게임에서 사용할 상수 객체 설정
hwinkr Mar 2, 2024
db43665
feat(LottoController): dom, domain을 연결하는 로또 게임 컨트롤러 구현
hwinkr Mar 2, 2024
0e1de6e
feat(event.js): 웹 기반 로또 게임에서 발생하는 이벤트 핸들러 등록 함수 구현
hwinkr Mar 2, 2024
651aa12
feat(LottoPurchaseForm): 로또 구입 폼 뷰 구현
hwinkr Mar 2, 2024
db25f4b
feat(WinningLottoForm): 당첨 로또 입력 폼 뷰 구현
hwinkr Mar 2, 2024
4296d99
feat(WinningResult): 당첨 결과 모달 뷰 구현
hwinkr Mar 2, 2024
228b01a
chore(add step1 domains): step1에서 구현한 로또 게임 도메인 로직 추가
hwinkr Mar 2, 2024
c6c5dec
feat(common styles): 공통으로 사용하는 css 분리
hwinkr Mar 2, 2024
52b2df0
feat(layout styles): 레이아웃 설정 css 구현
hwinkr Mar 2, 2024
25b3284
feat(reset styles): reset css
hwinkr Mar 2, 2024
0186fac
docs(REQUEST-STEP2.md): 기능 요구사항 완료
hwinkr Mar 2, 2024
7da2588
chore(add id): FormData를 활용하기 위한 id 추가
hwinkr Mar 6, 2024
ef38abb
refactor(LottoController): 구입한 로또 수량 인자 제거
hwinkr Mar 6, 2024
1d1e309
refactor(event): FormData 객체를 사용하는 것으로 변경
hwinkr Mar 6, 2024
78ef8af
refactor(LottoPurchaseForm): 스크롤 메시지 판단 로직 변경
hwinkr Mar 6, 2024
5aa3bf2
chore(common): game-container 스타일에 min-width 추가
hwinkr Mar 6, 2024
70d5477
chore(common): game-container 스타일에 min-width 추가
hwinkr Mar 6, 2024
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
node_modules/
dist/

75 changes: 75 additions & 0 deletions docs/REQUEST-STEP2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 🎱 웹 기반 로또 애플리케이션

## 도메인

### 로또

- 로또는 1 ~ 45의 6개의 숫자로 구성되어있다.

다음의 유효성 검증을 진행한다.

- [x] 숫자가 아닌 문자열이 들어오면 안된다.
- [x] 6개의 숫자로 구성되어야 한다.
- [x] 1 ~ 45 사이의 숫자여야 한다.
- [x] 로또는 중복된 숫자를 가질 수 없다

- [x] 로또 도메인은 당첨 로또와 비교했을 때, 몇개 일치하는지 판단하고 결과를 외부에 전달한다.

### 당첨 로또

- 당첨 로또는 6개의 로또 번호와 보너스 번호를 가진다.

다음의 유효성 검증을 진행한다.

- [x] 숫자가 아닌 문자열이 들어오면 안된다.
- [x] 6개의 숫자로 구성되어야 한다.
- [x] 1 ~ 45 사이의 숫자여야 한다.
- [x] 로또는 중복된 숫자를 가질 수 없다
- [x] 당첨 로또 6개와 보너스 번호는 중복될 수 없다.

### 로또 상점

- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
- 로또 1장의 가격은 1,000원이다.

다음의 유효성 검증을 진행한다.

- [x] 로또 가격은 문자열(공백)이 될 수 없다.
- [x] 로또 가격은 1,000으로 나누어떨어져야 한다.

### 로또 결과

- [x] 로또 결과판을 만든다.
- 등수와 각 등수의 갯수 정보로 구성되어있다.
- [x] 수익률을 계산한다.
- 수익률은 `총 당첨금/구입 금액`이다.
- 수익률은 반올림하여 소수점 2자리 수까지 계산한다.
- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.

```text
1등: 6개 번호 일치 / 2,000,000,000원
2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
3등: 5개 번호 일치 / 1,500,000원
4등: 4개 번호 일치 / 50,000원
5등: 3개 번호 일치 / 5,000원
```

## 입력

- [x] input 태그를 활용하여 로또 구입 금액을 입력받는다.
- [x] 유효하지 않은 입력의 경우 에러 메시지를 input 태그 아래에 표시한다

![alt text](image.png)

- [x] input 태그를 활용하여 지난 주 당첨 번호 6개와 보너스 번호를 입력 받는다.
- [x] 유효하지 않은 입력의 경우 에러 메시지를 input 태그 아래에 표시한다

![alt text](image-1.png)
Comment on lines +65 to +67
Copy link
Author

Choose a reason for hiding this comment

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

에러 메시지를 input 태그 아래에 표시하는 것은 기능 요구 사항을 적을 때는 고려했으나 구현은 하지 못했습니다. 체크 표시를 해버리고 제출했네요, 리뷰 요청을 하고나면 수정을 하면 안돼서 참고 부탁드립니다..! 😅


## 출력

- [x] 상단바에 **🎱 행운의 로또** UI를 구성한다.
- [x] 하단바에 **Copyright 2024 woowacourse** UI를 구성한다.
- [x] **🎱 내 당첨 번호 확인 🎱** UI를 구성한다.
- [x] 발행한 로또 번호 UI를 구성한다.
- [x] 팝업 UI를 활용하여 로또 당첨 결과와 수익률을 보여준다.
Binary file added docs/image-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 72 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,78 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>

<body>
<div id="app">
<h1>🎱 행운의 로또</h1>
</div>
<script type="module" src="./src/js/index.js"></script>
<header class="app-header">
<h1 class="text-lg font-bold text-v-center">🎱 행운의 로또</h1>
</header>

<main class="app-main">
<section class="h-80 w-30 game-container">
<h1 class="text-m font-bold text-h-center">🎱 내 번호 당첨 확인 🎱</h1>

<form id="lotto-purchase-form" class="flex flex-col purchase-form">
<label for="lotto-purchase-input" class="text-sm font-light"
>구입할 금액을 입력해주세요.</label
>
<div class="flex justify-between purchase-input-container">
<input
type="number"
placeholder="금액"
id="lotto-purchase-input"
name="lotto-purchase-input"
class="text-sm font-light"
required
autofocus
/>
<button type="submit" class="navy-button">구입</button>
</div>
</form>

<section
id="purchased-lottos-container"
class="lottos-container"
></section>

<form id="winning-lotto-form" class="hidden winning-lotto-form">
<span class="text-sm font-light"
>지난 주 당첨번호 6개와 보너스 번호 1개를 입력해주세요.</span
>
<div class="flex justify-between">
<span class="text-sm font-light">당첨 번호</span>
<span class="text-sm font-light">보너스 번호</span>
</div>
<div class="flex justify-between">
<div id="winning-lotto-input-container" class="flex gap-x-1"></div>
<div id="bonus-number-input-container"></div>
</div>
<button type="submit" class="navy-button w-100">결과 확인하기</button>
</form>
</section>
</main>

<section
class="hidden modal-background"
id="winning-result-modal-background"
>
<div class="flex flex-col gap-x-1 modal-content">
<button id="modal-cancel-button" class="cancel-button text-m">X</button>
<h1 class="text-m font-bold text-h-center">🏆 당첨 통계 🏆</h1>
<table
id="winning-result-content"
class="gap-x-1 result-container"
></table>
<span id="return-rate-container" class="text-sm font-bold"></span>
<button class="navy-button w-90" id="restart-button">
다시 시작하기
</button>
</div>
</section>

<footer class="app-footer">
<span class="text-sm color-navy font-light"
>Copyright 2024. woowacourse</span
>
</footer>
<script type="module" src="./src/step2-index.js"></script>
</body>
</html>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"test": "jest --watch --no-cache",
"build-step1": "webpack --config step1.config.js",
"start-step1": "npm run build-step1 && node dist/step1-bundle.js",
"start-step2": "webpack serve --open --config step2.config.js"
"start-step2": "webpack serve --open --config step2.config.js",
"build-step2": "webpack --mode production --config step2.config.js"
},
"devDependencies": {
"@babel/cli": "^7.20.7",
Expand Down
25 changes: 21 additions & 4 deletions src/step2-index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
/**
* step 2의 시작점이 되는 파일입니다.
* 노드 환경에서 사용하는 readline 등을 불러올 경우 정상적으로 빌드할 수 없습니다.
*/
import "./step2/styles/reset.css";
import "./step2/styles/layout.css";
import "./step2/styles/common.css";
import LottoController from "./step2/controllers/LottoController";
import {
registerCloseModalEvent,
registerPurchaseEvent,
registerRenderResultEvent,
registerRestartEvent,
} from "./step2/dom/event";

const lottoController = new LottoController();

registerPurchaseEvent(lottoController.purchaseLottos.bind(lottoController));
registerRenderResultEvent(
lottoController.renderWinningResult.bind(lottoController),
);
registerCloseModalEvent(
lottoController.closeWinningResultModal.bind(lottoController),
);
registerRestartEvent(lottoController.restartLottoGame.bind(lottoController));
33 changes: 33 additions & 0 deletions src/step2/constants/lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const ERROR_MESSAGES = {
invalidNumbersType: "로또 번호는 1~45 사이의 숫자여야 합니다.",
invalidLottoLength: "로또 번호는 6개여야 합니다.",
invalidLottoUniqueness: "로또 번호는 중복될 수 없습니다.",
invalidPurchaseAmount: "구입 금액은 1000단위의 숫자여야 합니다.",
invalidBonusNumberType: "보너스 번호는 1~45 사이의 숫자여야 합니다.",
invalidBonusNumberUniqueness: "보너스 번호는 로또 번호와 중복될 수 없습니다.",
invalidRetrySign: "재시작 신호는 y또는 n이어야 합니다.",
};

export const LOTTO_RULES = {
price: 1000,
length: 6,
minRandomNumber: 1,
maxRandomNumber: 45,
scrollThreadhold: 5,
};

export const ELEMENT_SELECTOR = {
purchaseForm: "lotto-purchase-form",
purchaseInput: "lotto-purchase-input",
purchasedLottoContainer: "purchased-lottos-container",
winningLottoForm: "winning-lotto-form",
winningLottoContainer: "winning-lotto-input-container",
winningLottoInput: ".winning-lotto-input",
bonusNumberContainer: "bonus-number-input-container",
bonusNumberInput: "bonus-number-input",
modalBackground: "winning-result-modal-background",
modalCancelButton: "modal-cancel-button",
restartButton: "restart-button",
winningResultContent: "winning-result-content",
returnRateContainer: "return-rate-container",
};
81 changes: 81 additions & 0 deletions src/step2/controllers/LottoController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import LottoStore from "../domains/LottoStore";
import WinningLotto from "../domains/WinningLotto";
import LottoResult from "../domains/LottoResult";
import Lotto from "../domains/Lotto";
import LottoPurchaseForm from "../dom/views/LottoPurcaseForm";
import WinningLottoForm from "../dom/views/WinningLottoForm";
import WinningResult from "../dom/views/WinningResult";
import { LOTTO_RULES } from "../constants/lotto";

export default class LottoController {
#lottos;

constructor() {
this.#lottos = null;
}

#isInstanceOfWinningLotto(winningLotto) {
return winningLotto instanceof WinningLotto;
}

#renderPurchasedLotto() {
LottoPurchaseForm.resetPurchaseForm();
const lottoNumbers = this.#lottos.map((lotto) => lotto.getNumbers());
LottoPurchaseForm.renderPurchasedLottos(lottoNumbers);

Choose a reason for hiding this comment

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

👍

}

#configWinningLotto(winningNumbers, bonusNumber) {
try {
const winninbNumberArray = winningNumbers.map((winningNumber) =>
Number(winningNumber)
);
return new WinningLotto(
new Lotto(winninbNumberArray),
Number(bonusNumber)
);
} catch (error) {
alert(error.message);
WinningLottoForm.resetWinningLottoForm();
WinningLottoForm.focusFirstWinningLottoInput();
}
}

purchaseLottos(purchaseAmount) {
try {
this.#lottos = LottoStore.purchaseLottos(purchaseAmount);
this.#renderPurchasedLotto();
WinningLottoForm.renderWinningLottoForm();
} catch (error) {
alert(error.message);
LottoPurchaseForm.resetPurchaseForm();
LottoPurchaseForm.focusPurchaseInput();
}
}

renderWinningResult(winningNumbers, bonusNumber) {
const winningLotto = this.#configWinningLotto(winningNumbers, bonusNumber);
if (!this.#isInstanceOfWinningLotto(winningLotto)) return;

const lottoResult = new LottoResult();
lottoResult.generateResult(this.#lottos, winningLotto);
const lottoRankBoard = lottoResult.getRankBoard();
const returnRate = lottoResult.calculateReturnRate(
this.#lottos.length * LOTTO_RULES.price
);

WinningResult.renderWinningResult(lottoRankBoard, returnRate);
}

closeWinningResultModal() {
WinningResult.closeResultModal();
}

restartLottoGame() {
this.closeWinningResultModal();
LottoPurchaseForm.resetPurchaseForm();
LottoPurchaseForm.focusPurchaseInput();
LottoPurchaseForm.removePurchasedLottos();
WinningLottoForm.resetWinningLottoForm();
WinningLottoForm.hideWinningLottoForm();
}
}
73 changes: 73 additions & 0 deletions src/step2/dom/event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ELEMENT_SELECTOR } from "../constants/lotto";

export const registerPurchaseEvent = (purchaseCallback) => {
const lottoPurchaseForm = document.getElementById(
ELEMENT_SELECTOR.purchaseForm
);

lottoPurchaseForm.addEventListener("submit", (event) => {
event.preventDefault();

const formData = new FormData(lottoPurchaseForm);
const lottoPurchaseAmount = formData.get("lotto-purchase-input");
purchaseCallback(lottoPurchaseAmount);
});
};

const getWinningLottoNumbers = () => {
const winningLottoInputs = document.querySelectorAll(
ELEMENT_SELECTOR.winningLottoInput
);

return [...winningLottoInputs].map((winningNumber) => {
return winningNumber.value;
});
};

const getBonusNumber = () => {
const bonusNumber = document.getElementById(
ELEMENT_SELECTOR.bonusNumberInput
);

return bonusNumber.value;
};

export const registerRenderResultEvent = (renderResultCallback) => {
const winningLottoForm = document.getElementById(
ELEMENT_SELECTOR.winningLottoForm
);

winningLottoForm.addEventListener("submit", (event) => {
event.preventDefault();
const winningNumbers = getWinningLottoNumbers();
const bonusNumber = getBonusNumber();
renderResultCallback(winningNumbers, bonusNumber);
});
};

export const registerCloseModalEvent = (closeCallback) => {
const modalBackground = document.getElementById(
ELEMENT_SELECTOR.modalBackground
);
const modalCancelButton = document.getElementById(
ELEMENT_SELECTOR.modalCancelButton
);

modalCancelButton.addEventListener("click", () => {
closeCallback();
});

modalBackground.addEventListener("click", (event) => {
if (event.target === event.currentTarget) {
closeCallback();
}
});
};

export const registerRestartEvent = (restartCallback) => {
const restartButton = document.getElementById(ELEMENT_SELECTOR.restartButton);

restartButton.addEventListener("click", () => {
restartCallback();
});
};
Loading