Skip to content

Commit

Permalink
[1단계 - 행운의 로또미션] 하루(김하루) 미션 제출합니다. (#8)
Browse files Browse the repository at this point in the history
* docs: 1단계 구현 기능 목록 작성

* chore: 코드포매터, 린터 초기 설정

Co-authored-by: JUNMO HAN <wnsah052@naver.com>

* feat: 앱 전체 구조 설정

Co-authored-by: JUNMO HAN <wnsah052@naver.com>

* fix: index.html 경로 수정

Co-authored-by: 365kim <365kim@users.noreply.github.com>

* chore: lottie 설치, 린터 설정 추가

Co-authored-by: 365kim <365kim@users.noreply.github.com>

* docs: 구현 기능 목록 수정

Co-authored-by: 365kim <365kim@users.noreply.github.com>

* feat: 구입금액 입력 컴포넌트 구현

Co-authored-by: 365kim <365kim@users.noreply.github.com>

* feat: 로또 발급 및 확인 컴포넌트 구현

Co-authored-by: 365kim <365kim@users.noreply.github.com>

* refactor: 삼항연산자 대신 단축평가 사용

Co-authored-by: JUNMO HAN <wnsah052@naver.com>

* feat: 표시되는 로또 번호의 자릿수 맞춤

* refactor: 로또 배열의 key값 수정

Co-authored-by: JUNMO HAN <wnsah052@naver.com>

* feat: 당첨번호 발표 컴포넌트 구현

Co-authored-by: JUNMO HAN <wnsah052@naver.com>

* feat: 당첨결과 확인 컴포넌트 구현

Co-authored-by: JUNMO HAN <wnsah052@naver.com>

* refactor: 당첨번호 JSON에서 가져오도록 변경

Co-authored-by: JUNMO HAN <wnsah052@naver.com>

* feat: 로또볼 CSS 스타일링 및 메인색상 적용

* feat: 애니메이션 구현

Co-authored-by: 365kim <365kim@users.noreply.github.com>

* feat: 구매한 복권별 당첨 여부 확인 기능 구현

* chore: 회차 당첨번호 현행화

* refactor: 변수명 수정

* fix: 콘솔 에러 수정

* refactor: 컴포넌트 구조 변경

* refactor: 재정렬 가능성 없는 배열의 key값 index로 설정

* refactor: getMatchCount 함수에 reduce 활용

* refactor: getStatistics -> getComputedResult 이름 변경

* refactor: shouldPlayAnimation -> isLoading 이름 변경

* refactor: 버튼 스타일 변경

* refactor: 불필요한 클래스 삭제

* refactor: HTML 태그 변경 및 삭제

* refactor: CSS 클래스 제어 및 BEM 네이밍 적용

* refactor: App.js에 initialState 추가

* refactor: 리팩토링에 따른 네이밍 통일

* refactor: <Lottie /> props 스프레드로 전달

* refactor: let 대신 const 사용

* refactor: props 중 자식요소 네이밍 변경

Co-authored-by: Jbee <ljyhanll@gmail.com>

* refactor: props 중 자식요소 네이밍 변경

Co-authored-by: Jbee <ljyhanll@gmail.com>

* refactor: 버튼의 필수 props 외에는 스프레드로 전달

Co-authored-by: Jbee <ljyhanll@gmail.com>

* chore: 브랜치 병합

* refactor: Classnames 라이브러리 적용

* refactor: String 내장메서드 padStart 적용

* refactor: Plain/SubmitButton -> Button컴포넌트로 통합

* refactor: <Record> 생성, <ResultSummary> 삭제

Co-authored-by: JUNMO HAN <wnsah052@naver.com>
Co-authored-by: 365kim <365kim@users.noreply.github.com>
Co-authored-by: Jbee <ljyhanll@gmail.com>
  • Loading branch information
4 people committed Apr 19, 2021
1 parent be409b4 commit 174ea4c
Show file tree
Hide file tree
Showing 51 changed files with 12,926 additions and 15 deletions.
27 changes: 27 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"env": {
"browser": true
},
"extends": ["airbnb", "airbnb/hooks", "plugin:prettier/recommended"],
"plugins": ["prettier"],
"parserOptions": {
"sourceType": "module"
},
"parser": "babel-eslint",
"rules": {
"no-new": "off",
"no-alert": "off",
"no-param-reassign": "off",
"no-return-assign": "off",
"import/extensions": "off",
"import/prefer-default-export": "off",
"max-depth": ["error", 1],
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/jsx-one-expression-per-line": "off",
"react/prefer-stateless-function": "off",
"react/prop-types": "off",
"react/destructuring-assignment": "off",
"max-classes-per-file": ["error", 3]
}
}
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

# editor
.vscode/
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"semi": true,
"useTabs": false,
"tabWidth": 2,
"printWidth": 100,
"endOfLine": "auto",
"singleQuote": true,
"arrowParens": "always",
"trailingComma": "all"
}
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,33 @@
- Boilerplate는 페어와 협의하여 자유롭게 선택합니다.
- CSS는 레벨1에 있는 코드를 직접 찾아서 자유롭게 재활용합니다.
- 불필요한 Third-Party 없이 React에서 제공되는 것만으로 구현합니다.
- 테스트 도구 선정부터 작성까지 일절 관여하지 않습니다. 자유롭게 즐겨보세요
- 테스트 도구 선정부터 작성까지 일절 관여하지 않습니다. 자유롭게 즐겨보세요.
<br>

## 📝 Requirements

### Step1

- [ ] `Class Component`를 사용합니다.
- [ ] 로또 구입 금액을 입력하면, 금액에 해당하는 로또를 발급해야 한다.
- [ ] 로또 1장의 가격은 1,000원이다.
- [ ] 소비자는 **자동 구매**만 할 수 있다.
- [ ] 복권 번호는 번호보기 토글 버튼을 클릭하면, 볼 수 있어야 한다.
- [ ] 결과 확인하기 버튼을 누르면 당첨 통계, 수익률을 모달로 확인할 수 있다.
- [ ] 로또 당첨 금액은 고정되어 있는 것으로 가정한다.
- [ ] 다시 시작하기 버튼을 누르면 초기화 되서 다시 구매를 시작할 수 있다.
- 리액트 제한사항
- [x] 클래스 컴포넌트를 사용한다.
- 구입금액 입력
- [x] 로또 구입 금액을 입력하면, 유효성 검사 결과를 실시간으로 표시한다.
- [x] 최소 화폐단위(1원) 미만의 자릿수가 포함된 경우
- [x] 로또 1장의 가격(1,000원) 미만일 경우
- [x] 로또 1장의 가격(1,000원)으로 나누어 떨어지지 않을 경우
- 로또 발급 및 확인
- [x] 로또 구입 금액을 제출하면, 자동구매로 금액에 해당하는 로또를 발급한다.
- [x] 번호보기 토글 버튼을 클릭하면 복권번호를 볼 수 있다.
- 당첨번호 발표
- [x] 로또 발급 후 카운트 다운(3초)후에 이번주 당첨번호와 결과확인 버튼이 표시된다.
- 당첨결과 확인
- [x] 결과 확인하기 버튼을 누르면 구매한 복권의 당첨여부 및 수익률을 모달로 표시된다.
- [x] 로또 당첨 금액은 고정되어 있는 것으로 가정한다.
- [x] 다시 시작하기 버튼을 누르면 초기화 되서 다시 구매를 시작할 수 있다.

### Step2

- [ ] Step1의 `Class Component``Function Component`로 마이그레이션 합니다.

### 공통 심화

- [ ] UI를 통해 **실시간으로** 당첨 번호 발표까지 남은 시간을 제공합니다.
- [ ] 다시 시작하기 버튼을 누르면 당첨 번호 발표 시간도 사라진다.
- [ ] Step1의 클래스 컴포넌트를 함수형 컴포넌트로 마이그레이션 한다.

## 👏 Contributing

Expand Down
48 changes: 48 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "1st-react-lotto",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"babel-eslint": "^10.1.0",
"classnames": "^2.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-lottie": "^1.2.3",
"react-scripts": "4.0.3",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"eslint": "^7.24.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"prettier": "^2.2.1"
}
}
12 changes: 12 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Lotto App" />
<title>🎱 행운의 로또</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
88 changes: 88 additions & 0 deletions src/components/App/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable react/sort-comp */
import { Component } from 'react';
import PurchaseForm from '../containers/PurchaseForm';
import UserLotto from '../containers/UserLotto';
import WinningNumbers from '../containers/WinningNumbers';
import UserResult from '../containers/UserResult';
import { createLotto } from './service';
import './style.css';

const initialState = {
lottoBundle: [],
winningNumber: {},
shouldReset: false,
isShowingUserResult: false,
};
export default class App extends Component {
constructor() {
super();

this.state = { ...initialState };
this.onPurchaseLotto = this.onPurchaseLotto.bind(this);
this.setWinningNumber = this.setWinningNumber.bind(this);
this.onShowUserResult = this.onShowUserResult.bind(this);
this.onCloseUserResult = this.onCloseUserResult.bind(this);
this.onReset = this.onReset.bind(this);
this.didReset = this.didReset.bind(this);
}

onPurchaseLotto({ numOfLotto }) {
this.setState({ lottoBundle: [...Array(numOfLotto)].map(() => createLotto()) });
}

setWinningNumber({ winningNumber }) {
this.setState({ winningNumber });
}

onShowUserResult() {
this.setState({ isShowingUserResult: true });
}

onCloseUserResult() {
this.setState({ isShowingUserResult: false });
}

onReset() {
this.setState({ ...initialState, shouldReset: true });
}

didReset() {
this.setState({ shouldReset: false });
}

render() {
const { lottoBundle, winningNumber, isShowingUserResult, shouldReset } = this.state;
const isPurchased = Boolean(lottoBundle.length);

return (
<>
<main className="App__main">
<h1 className="App__title">행운의 로또</h1>
<PurchaseForm
lottoBundle={lottoBundle}
onPurchaseLotto={this.onPurchaseLotto}
shouldReset={shouldReset}
didReset={this.didReset}
/>
{isPurchased && (
<>
<UserLotto lottoBundle={this.state.lottoBundle} />
<WinningNumbers
setWinningNumber={this.setWinningNumber}
onShowUserResult={this.onShowUserResult}
/>
</>
)}
</main>
{isShowingUserResult && (
<UserResult
lottoBundle={lottoBundle}
winningNumber={winningNumber}
onCloseUserResult={this.onCloseUserResult}
onReset={this.onReset}
/>
)}
</>
);
}
}
15 changes: 15 additions & 0 deletions src/components/App/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getRandomNumber } from '../../utils';
import { LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER, LOTTO_NUMBERS_LENGTH } from '../../constants';

export const createLotto = (array = []) => {
const number = getRandomNumber({ min: LOTTO_MIN_NUMBER, max: LOTTO_MAX_NUMBER });

if (array.length === LOTTO_NUMBERS_LENGTH) {
return array.sort((a, b) => a - b);
}
if (!array.includes(number)) {
array.push(number);
}

return createLotto(array);
};
19 changes: 19 additions & 0 deletions src/components/App/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.App__main {
position: absolute;
top: 3rem;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.005);
padding: 3.5rem 2.5rem;
border-radius: 1.5rem;
width: 400px;
min-height: 440px;
box-shadow: 6px 10px 20px rgb(0, 0, 0, 0.15);
}

.App__title {
text-align: center;
color: #333;
font-size: 1.5rem;
margin: 0;
}
88 changes: 88 additions & 0 deletions src/components/containers/PurchaseForm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import React, { Component } from 'react';
import { Button } from '../../shared';
import { validatePurchaseAmount, payForLotto } from './service';
import { MESSAGE } from '../../../constants';
import './style.css';

export default class PurchaseForm extends Component {
constructor(props) {
super(props);

this.state = {
validationMessage: '',
isInputDisabled: false,
isSubmitButtonDisabled: true,
};
this.paymentInput = React.createRef();
this.onChangeInput = this.onChangeInput.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}

componentDidMount() {
this.paymentInput.current.focus();
}

componentDidUpdate() {
if (this.props.shouldReset) {
this.paymentInput.current.value = '';
this.paymentInput.current.disabled = false;
this.paymentInput.current.focus();
this.props.didReset();
}
}

onSubmit(e) {
e.preventDefault();

const money = e.target.input.value;
const { change, numOfLotto } = payForLotto(money);

if (change > 0) {
alert(MESSAGE.PURCHASE_AMOUNT_HAS_CHANGE(change));
}

this.props.onPurchaseLotto({ numOfLotto });
this.setState({ isInputDisabled: true, isSubmitButtonDisabled: true });
}

onChangeInput(e) {
const money = e.target.value;
const { validationMessage, isSubmitButtonDisabled } = validatePurchaseAmount(money);

this.setState({ validationMessage, isSubmitButtonDisabled });
}

render() {
const { validationMessage, isInputDisabled, isSubmitButtonDisabled } = this.state;

return (
<div>
<form className="PurchaseForm" onSubmit={this.onSubmit}>
<label className="PurchaseForm__label">
<span className="PurchaseForm__text">구입할 금액을 입력해주세요.</span>
<input
className="PurchaseForm__input"
name="input"
type="number"
placeholder="구입 금액"
onChange={this.onChangeInput}
ref={this.paymentInput}
disabled={isInputDisabled}
/>
</label>
<div className="PurchaseForm__button_wrapper">
<Button
type="submit"
className="PurchaseForm__button"
disabled={isSubmitButtonDisabled}
>
구매
</Button>
</div>
</form>
<div className="ValidationMessage">{validationMessage}</div>
</div>
);
}
}

0 comments on commit 174ea4c

Please sign in to comment.