-
Notifications
You must be signed in to change notification settings - Fork 37
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단계 - 계산기] 호프(김문희) 미션 제출합니다. #46
Changes from all commits
76e6515
57e6f3e
e99bf93
ce934dd
1e4fb93
694e7aa
20fd557
3724887
1334795
b626bfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,177 +1,118 @@ | ||
import { Component } from 'react'; | ||
import { | ||
useLayoutEffect, | ||
useEffect, | ||
useState, | ||
useRef, | ||
useCallback, | ||
} from 'react'; | ||
import '../styles/Calculator.css'; | ||
import { computeExpression, hasInput, computeNextOperand } from '../utils'; | ||
import Expression from './Expression'; | ||
import Digits from './Digits'; | ||
import Modifier from './Modifier'; | ||
import Operations from './Operations'; | ||
|
||
const computeExpression = ({ firstOperand, secondOperand, operation }) => { | ||
if (operation === '/') { | ||
return Math.floor(firstOperand / secondOperand); | ||
} | ||
if (operation === 'X') { | ||
return firstOperand * secondOperand; | ||
} | ||
if (operation === '-') { | ||
return firstOperand - secondOperand; | ||
} | ||
if (operation === '+') { | ||
return firstOperand + secondOperand; | ||
} | ||
}; | ||
|
||
const hasInput = ({ firstOperand, secondOperand, operation }) => | ||
firstOperand !== '0' || secondOperand !== '' || operation !== null; | ||
const Calculator = () => { | ||
const [state, setState] = useState({ | ||
firstOperand: 0, | ||
secondOperand: -1, | ||
operation: null, | ||
isError: false, | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하나의 state에 모든 정보를 객체형태로 넣는 것은 안티패턴입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 일단 계산기 내에서 각각 상태값이 거의 함께 변경되어서 객체형태로 수정해주었는데요, |
||
|
||
const computeNextOperand = (currentOperand, digit) => { | ||
return currentOperand.length >= 3 | ||
? `${currentOperand.slice(0, -1)}${digit}` | ||
: `${Number(currentOperand + digit)}`; | ||
}; | ||
const ref = useRef(null); | ||
|
||
class Calculator extends Component { | ||
constructor() { | ||
super(); | ||
useLayoutEffect(() => { | ||
const memoizedState = JSON.parse(localStorage.getItem('prevState')); | ||
this.state = memoizedState | ||
? memoizedState | ||
: { | ||
firstOperand: '0', | ||
secondOperand: '', | ||
operation: null, | ||
isError: false, | ||
}; | ||
} | ||
if (!memoizedState) return; | ||
setState(memoizedState); | ||
}, []); | ||
|
||
componentDidMount() { | ||
window.addEventListener('beforeunload', this.onBeforeUnload); | ||
} | ||
useEffect(() => { | ||
ref.current = state; | ||
}, [state]); | ||
|
||
componentWillUnmount() { | ||
window.removeEventListener('beforeunload', this.onBeforeUnload); | ||
localStorage.setItem('prevState', JSON.stringify(this.state)); | ||
} | ||
useEffect(() => { | ||
window.addEventListener('beforeunload', handleWindowClose); | ||
return () => window.removeEventListener('beforeunload', handleWindowClose); | ||
}, []); | ||
|
||
initState = () => { | ||
this.setState({ | ||
firstOperand: '0', | ||
secondOperand: '', | ||
operation: null, | ||
isError: false, | ||
}); | ||
const handleWindowClose = (e) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 함수를 어떤 이유에서 이렇게 작성하셨는지는 이해하겠습니다. 다만 함수의 쓰임새가 useEffect 안에서만 이뤄지고 있으니, useEffect 안으로 옮기시는 편이 더 나을 것 같아요. 현재 상태로는 Calculator가 render될 때마다 새롭게 정의되는 거거든요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵! 일단 함수를 다 분리한다는 목적으로 했는데, 렌더될때마다 함수가 새로 생성되어서 성능상 좋지 못하다는 것은 간과했네요 😭 지적해주셔서 감사합니다. |
||
e.preventDefault(); | ||
if (!hasInput(ref.current)) return; | ||
localStorage.setItem('prevState', JSON.stringify(ref.current)); | ||
e.returnValue = ''; | ||
}; | ||
|
||
onClickDigit = ({ target }) => { | ||
const { textContent: digit } = target; | ||
this.state.operation | ||
? this.setState(({ secondOperand }) => ({ | ||
secondOperand: computeNextOperand(secondOperand, digit), | ||
isError: false, | ||
const onClickDigit = (digit) => () => { | ||
state.operation | ||
? setState((prevState) => ({ | ||
...prevState, | ||
secondOperand: computeNextOperand( | ||
prevState.secondOperand === -1 ? 0 : prevState.secondOperand, | ||
digit, | ||
), | ||
})) | ||
: this.setState(({ firstOperand }) => ({ | ||
firstOperand: computeNextOperand(firstOperand, digit), | ||
isError: false, | ||
: setState((prevState) => ({ | ||
...prevState, | ||
firstOperand: computeNextOperand(prevState.firstOperand, digit), | ||
})); | ||
}; | ||
|
||
onClickOperation = ({ target }) => { | ||
const { textContent: operation } = target; | ||
const onClickOperation = (operation) => () => { | ||
if (operation !== '=') { | ||
this.setState({ operation }); | ||
setState((prevState) => ({ | ||
...prevState, | ||
operation, | ||
})); | ||
return; | ||
} | ||
|
||
const result = computeExpression({ | ||
firstOperand: Number(this.state.firstOperand), | ||
secondOperand: Number(this.state.secondOperand), | ||
operation: this.state.operation, | ||
firstOperand: Number(state.firstOperand), | ||
secondOperand: Number(state.secondOperand), | ||
operation: state.operation, | ||
}); | ||
|
||
if (isFinite(result)) { | ||
this.setState({ | ||
firstOperand: `${result}`, | ||
secondOperand: '', | ||
setState((prevState) => ({ | ||
...prevState, | ||
firstOperand: result, | ||
secondOperand: -1, | ||
operation: null, | ||
}); | ||
})); | ||
return; | ||
} | ||
|
||
this.setState(() => ({ | ||
isError: true, | ||
setState((prevState) => ({ | ||
...prevState, | ||
isError: false, | ||
})); | ||
}; | ||
|
||
onBeforeUnload = (e) => { | ||
e.preventDefault(); | ||
localStorage.setItem('prevState', JSON.stringify(this.state)); | ||
if (hasInput(this.state)) { | ||
e.returnValue = ''; | ||
} | ||
}; | ||
const onClickModifier = useCallback(() => { | ||
setState({ | ||
firstOperand: 0, | ||
secondOperand: -1, | ||
operation: null, | ||
isError: false, | ||
}); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
<div>숫자는 3자리 까지만 입력이 가능합니다.</div> | ||
<div className="calculator"> | ||
<Expression | ||
isError={state.isError} | ||
firstOperand={state.firstOperand} | ||
operation={state.operation} | ||
secondOperand={state.secondOperand} | ||
/> | ||
<Digits onClick={onClickDigit} /> | ||
<Modifier onClick={onClickModifier} /> | ||
<Operations onClick={onClickOperation} /> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
render() { | ||
return ( | ||
<> | ||
<div>숫자는 3자리 까지만 입력이 가능합니다.</div> | ||
<div className="calculator"> | ||
<h1 id="total"> | ||
{this.state.isError | ||
? '오류' | ||
: `${this.state.firstOperand} | ||
${this.state.operation ?? ''} | ||
${this.state.secondOperand}`} | ||
</h1> | ||
<div className="digits flex"> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
9 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
8 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
7 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
6 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
5 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
4 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
3 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
2 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
1 | ||
</button> | ||
<button className="digit" onClick={this.onClickDigit}> | ||
0 | ||
</button> | ||
</div> | ||
<div className="modifiers subgrid" onClick={this.initState}> | ||
<button className="modifier">AC</button> | ||
</div> | ||
<div className="operations subgrid"> | ||
<button className="operation" onClick={this.onClickOperation}> | ||
/ | ||
</button> | ||
<button className="operation" onClick={this.onClickOperation}> | ||
X | ||
</button> | ||
<button className="operation" onClick={this.onClickOperation}> | ||
- | ||
</button> | ||
<button className="operation" onClick={this.onClickOperation}> | ||
+ | ||
</button> | ||
<button className="operation" onClick={this.onClickOperation}> | ||
= | ||
</button> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} | ||
} | ||
export default Calculator; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import PropTypes from 'prop-types'; | ||
const DIGITS = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; | ||
|
||
const Digits = ({ onClick }) => { | ||
return ( | ||
<div className="digits flex"> | ||
{DIGITS.map((digit) => ( | ||
<button className="digit" onClick={onClick(digit)} key={digit}> | ||
{digit} | ||
</button> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
Digits.propTypes = { | ||
onClick: PropTypes.func, | ||
}; | ||
|
||
export default Digits; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import PropTypes from 'prop-types'; | ||
|
||
const Expression = ({ isError, firstOperand, operation, secondOperand }) => { | ||
return ( | ||
<h1 id="total"> | ||
{isError | ||
? '오류' | ||
: `${firstOperand} | ||
${operation ?? ''} | ||
${secondOperand < 0 ? '' : secondOperand}`} | ||
</h1> | ||
); | ||
}; | ||
|
||
Expression.propTypes = { | ||
isError: PropTypes.bool, | ||
firstOperand: PropTypes.number, | ||
secondOperand: PropTypes.number, | ||
operation: PropTypes.string, | ||
}; | ||
export default Expression; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import PropTypes from 'prop-types'; | ||
import { memo } from 'react'; | ||
|
||
const Modifier = ({ onClick }) => { | ||
return ( | ||
<div className="modifiers subgrid"> | ||
<button className="modifier" onClick={onClick}> | ||
AC | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
Modifier.propTypes = { | ||
onClick: PropTypes.func, | ||
}; | ||
|
||
export default memo(Modifier); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import PropTypes from 'prop-types'; | ||
const OPERATIONS = ['/', 'X', '-', '+', '=']; | ||
|
||
const Operations = ({ onClick }) => { | ||
return ( | ||
<div className="operations subgrid"> | ||
{OPERATIONS.map((operation) => ( | ||
<button | ||
className="operation" | ||
onClick={onClick(operation)} | ||
key={operation} | ||
> | ||
{operation} | ||
</button> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
Operations.propTypes = { | ||
onClick: PropTypes.func, | ||
}; | ||
|
||
export default Operations; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
const calculateByOperation = { | ||
'/': (firstOperand, secondOperand) => | ||
Math.floor(firstOperand / secondOperand), | ||
X: (firstOperand, secondOperand) => firstOperand * secondOperand, | ||
'-': (firstOperand, secondOperand) => firstOperand - secondOperand, | ||
'+': (firstOperand, secondOperand) => firstOperand + secondOperand, | ||
}; | ||
|
||
export const computeExpression = ({ | ||
firstOperand, | ||
secondOperand, | ||
operation, | ||
}) => { | ||
const calculator = calculateByOperation[operation]; | ||
if (!calculator) throw new Error('잘못된 연산자를 입력하였습니다.'); | ||
return calculator(firstOperand, secondOperand); | ||
}; | ||
|
||
export const hasInput = ({ firstOperand, secondOperand, operation }) => | ||
firstOperand !== 0 || secondOperand !== -1 || operation !== null; | ||
|
||
export const computeNextOperand = (currentOperand, digit) => { | ||
currentOperand = String(currentOperand); | ||
return currentOperand.length >= 3 | ||
? Number(`${currentOperand.slice(0, -1)}${digit}`) | ||
: Number(currentOperand + digit); | ||
}; |
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.
초기값이 -1인 이유는 뭔가요?
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.
이 부분 뒷자리 숫자가 '없다'라는 것을 표현해주기 위해서 일단 -1로 초기화해주었는데요, 0 으로 바꾸는게 더 맞다고 판단됩니다. 지적해주셔서 감사합니다.