2025-12-24 15:05:17 +09:00

498 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useEffect, forwardRef } from 'react'
import { createCalculator } from '@/util/calc-utils'
import '@/styles/calc.scss'
export const CalculatorInput = forwardRef(
({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder, name='', disabled = false, maxLength = 12 }, ref) => {
const [showKeypad, setShowKeypad] = useState(false)
const [displayValue, setDisplayValue] = useState(value || '0')
const [hasOperation, setHasOperation] = useState(false)
const calculatorRef = useRef(createCalculator(options))
const containerRef = useRef(null)
const inputRef = useRef(null)
// 외부 ref와 내부 ref를 동기화
useEffect(() => {
if (ref) {
if (typeof ref === 'function') {
ref(inputRef.current)
} else {
ref.current = inputRef.current
}
}
}, [ref])
// Sync displayValue with value prop
useEffect(() => {
setDisplayValue(value || '0')
}, [value])
// 클릭 외부 감지
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setShowKeypad(false)
if (hasOperation) {
// If there's an operation in progress, compute the result when losing focus
handleCompute()
}
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [value, onChange, hasOperation])
// 숫자 입력 처리 함수 수정
const handleNumber = (num) => {
const calculator = calculatorRef.current
let newDisplayValue = ''
// maxLength 체크
if (maxLength > 0) {
const currentLength = (calculator.currentOperand || '').length + (calculator.previousOperand || '').length + (calculator.operation || '').length
if (currentLength >= maxLength) {
return
}
}
// 소수점 이하 2자리 제한 로직 추가
const shouldPreventInput = (value) => {
if (!value) return false
const decimalParts = value.toString().split('.')
return decimalParts.length > 1 && decimalParts[1].length >= 2
}
// 숫자 추가 함수
const appendNumber = (current, num) => {
// maxLength 체크
if (maxLength > 0 && (current + num).length > maxLength) {
return current
}
// 현재 값이 0이고 소수점이 없을 때 0이 아닌 숫자를 입력하면 대체
if (current === '0' && num !== '.' && !current.includes('.')) {
return num.toString()
}
// 0. 다음에 0을 입력하는 경우 허용
if (current === '0' && num === '0') {
return '0.'
}
return current + num
}
if (hasOperation) {
// 연산자 이후 숫자 입력 시
if (calculator.shouldResetDisplay) {
calculator.currentOperand = num.toString()
calculator.shouldResetDisplay = false
} else if (num === '.') {
if (!calculator.currentOperand.includes('.')) {
calculator.currentOperand = calculator.currentOperand || '0' + '.'
}
} else if (!shouldPreventInput(calculator.currentOperand)) {
calculator.currentOperand = appendNumber(calculator.currentOperand || '0', num)
}
newDisplayValue = calculator.previousOperand + calculator.operation + calculator.currentOperand
setDisplayValue(newDisplayValue)
} else {
// 첫 번째 숫자 입력 시
if (calculator.shouldResetDisplay) {
calculator.currentOperand = num.toString()
calculator.shouldResetDisplay = false
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue)
if (!hasOperation) {
onChange(calculator.currentOperand)
}
} else if (num === '.') {
if (!calculator.currentOperand.includes('.')) {
calculator.currentOperand = (calculator.currentOperand || '0') + '.'
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue)
if (!hasOperation) {
onChange(newDisplayValue)
}
}
} else if (!shouldPreventInput(calculator.currentOperand)) {
calculator.currentOperand = appendNumber(calculator.currentOperand || '0', num)
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue)
if (!hasOperation) {
onChange(newDisplayValue)
}
}
// else {
// calculator.currentOperand = (calculator.currentOperand || '') + num
// newDisplayValue = calculator.currentOperand
// setDisplayValue(newDisplayValue)
// if (!hasOperation) {
// onChange(newDisplayValue)
// }
// }
}
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = newDisplayValue.length
inputRef.current.setSelectionRange(len, len)
// 텍스트 끝으로 스크롤
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
// 연산자 처리 함수 수정
const handleOperation = (operation) => {
const calculator = calculatorRef.current
let newDisplayValue = ''
// 현재 입력된 값이 없으면 이전 값 사용 (연속 연산 시)
if (!calculator.currentOperand && calculator.previousOperand) {
calculator.operation = operation
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
setHasOperation(true)
} else if (hasOperation) {
// 이미 연산자가 있는 경우, 계산 실행
const result = calculator.compute()
if (result !== undefined) {
calculator.previousOperand = result.toString()
calculator.operation = operation
calculator.currentOperand = ''
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
}
} else {
// 첫 번째 연산자 입력 시
calculator.previousOperand = calculator.currentOperand || '0'
calculator.operation = operation
calculator.currentOperand = ''
setHasOperation(true)
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
}
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = newDisplayValue.length
inputRef.current.setSelectionRange(len, len)
// 텍스트 끝으로 스크롤
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
// AC 버튼 클릭 핸들러
const handleClear = () => {
const calculator = calculatorRef.current
const newValue = calculator.clear()
const displayValue = newValue || '0'
setDisplayValue(displayValue)
setHasOperation(false)
onChange(displayValue)
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = displayValue.length
inputRef.current.setSelectionRange(len, len)
// 텍스트 끝으로 스크롤
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
// 계산 실행 함수 수정
const handleCompute = (fromEnterKey = false) => {
const calculator = calculatorRef.current
if (!hasOperation || !calculator.currentOperand) return
const result = calculator.compute()
if (result !== undefined) {
const resultStr = result.toString()
setDisplayValue(resultStr)
setHasOperation(false)
// Only call onChange with the final result
onChange(resultStr)
// 엔터키로 호출된 경우 포커스 설정하지 않음
if (!fromEnterKey) {
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = resultStr.length
inputRef.current.setSelectionRange(len, len)
}
})
}
}
}
// DEL 버튼 클릭 핸들러
const handleDelete = () => {
const calculator = calculatorRef.current
const newValue = calculator.deleteNumber()
const displayValue = newValue || '0'
setDisplayValue(displayValue)
setHasOperation(!!calculator.operation)
onChange(displayValue)
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = displayValue.length
inputRef.current.setSelectionRange(len, len)
// 텍스트 끝으로 스크롤
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
// input의 onChange 이벤트 처리 - 허용된 계산기 입력만 처리
const handleInputChange = (e) => {
if (readOnly) return
const inputValue = e.target.value
// 허용된 문자만 필터링 (숫자, 연산자, 소수점)
const filteredValue = inputValue.replace(/[^0-9+\-×÷.]/g, '')
// 연산자 연속 입력 방지
const lastChar = filteredValue[filteredValue.length - 1]
const prevChar = filteredValue[filteredValue.length - 2]
if (['+', '×', '÷'].includes(lastChar) && ['+', '-', '×', '÷', '.'].includes(prevChar)) {
// 연산자나 소수점이 연속으로 입력된 경우 이전 문자 유지
return
}
// 소수점 중복 입력 방지
const parts = filteredValue.split(/[+\-×÷]/)
if (parts[parts.length - 1].split('.').length > 2) {
// 한 숫자에 소수점이 2개 이상인 경우
return
}
setDisplayValue(filteredValue)
// 계산기 상태 업데이트
if (filteredValue !== displayValue) {
const calculator = calculatorRef.current
const hasOperation = /[+\-×÷]/.test(filteredValue)
if (hasOperation) {
const [operand1, operator, operand2] = filteredValue.split(/([+\-×÷])/)
calculator.previousOperand = operand1 || ''
calculator.operation = operator || ''
calculator.currentOperand = operand2 || ''
setHasOperation(true)
} else {
calculator.currentOperand = filteredValue
setHasOperation(false)
}
onChange(filteredValue)
}
}
// 키패드 토글 함수
const toggleKeypad = (e) => {
if (e) {
e.preventDefault()
e.stopPropagation()
}
const newShowKeypad = !showKeypad
setShowKeypad(newShowKeypad)
// Show keypad 시에만 포커스 유지
if (newShowKeypad) {
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}
}
// 키보드 이벤트 처리 수정
const handleKeyDown = (e) => {
if (readOnly) return
// Tab 키는 계산기 숨기고 기본 동작 허용
if (e.key === 'Tab') {
setShowKeypad(false)
return
}
// 모든 방향키는 기본 동작 허용
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
setShowKeypad(true)
return
}
// 계산기 허용 키들이 입력되면 키패드 표시 (엔터키 제외)
if (/^[0-9+\-×÷.=]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete' || e.key === '*' || e.key === '/') {
setShowKeypad(true)
}
// 키패드가 숨겨진 상태에서 엔터키는 페이지로 전달
if (!showKeypad && e.key === 'Enter') {
return
}
e.preventDefault()
const calculator = calculatorRef.current
const { allowDecimal } = options
if (e.key === '.') {
// allowDecimal이 false이면 소수점 입력 무시
if (!allowDecimal) return
// 소수점 입력 처리
const currentValue = displayValue.toString()
const parts = currentValue.split(/[+\-×÷]/)
const lastPart = parts[parts.length - 1]
// 이미 소수점이 있으면 무시
if (!lastPart.includes('.')) {
handleNumber(e.key)
}
} else if (/^[0-9]$/.test(e.key)) {
handleNumber(e.key)
} else {
switch (e.key) {
case '+':
case '-':
case '*':
case '/':
const opMap = { '*': '×', '/': '÷' }
handleOperation(opMap[e.key] || e.key)
break
case 'Enter':
case '=':
if (showKeypad) {
// 키패드가 보이는 상태에서 엔터키: 계산 후 키패드만 숨김
handleCompute(true) // 엔터키로 호출됨을 표시
setShowKeypad(false)
}
// 키패드가 숨겨진 상태에서 엔터키: 페이지로 전달 (preventDefault 하지 않음)
break
case 'Backspace':
case 'Delete':
handleDelete()
break
case 'Escape':
handleClear()
setShowKeypad(false)
break
default:
break
}
}
}
return (
<div ref={containerRef} className="calculator-input-wrapper">
{label && (
<label htmlFor={id} className="calculator-label">
{label}
</label>
)}
<input
ref={inputRef}
type="text"
id={id}
name={name}
value={displayValue}
readOnly={readOnly}
className={className}
onClick={() => !readOnly && setShowKeypad(true)}
onFocus={() => !readOnly && setShowKeypad(true)}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
tabIndex={readOnly ? -1 : 0}
placeholder={placeholder}
autoComplete={'off'}
disabled={disabled}
maxLength={maxLength}
/>
{showKeypad && !readOnly && (
<div className="keypad-container">
<div className="keypad-grid">
<button
onClick={() => {
// const newValue = calculatorRef.current.clear()
// setDisplayValue(newValue || '0')
// onChange(newValue || '0')
handleClear()
setHasOperation(false)
}}
className="btn-clear"
>
AC
</button>
<button
onClick={() => {
// const newValue = calculatorRef.current.deleteNumber()
// setDisplayValue(newValue || '0')
// onChange(newValue || '0')
//setHasOperation(!!calculatorRef.current.operation)
handleDelete()
}}
className="btn-delete"
>
DEL
</button>
<button onClick={() => handleOperation('÷')} className="btn-operator">
÷
</button>
<button onClick={() => handleOperation('×')} className="btn-operator">
×
</button>
<button onClick={() => handleOperation('-')} className="btn-operator">
-
</button>
<button onClick={() => handleOperation('+')} className="btn-operator">
+
</button>
{/* 숫자 버튼 */}
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
<button key={num} onClick={() => handleNumber(num)} className="btn-number">
{num}
</button>
))}
{/* 0 버튼 */}
<button onClick={() => handleNumber('0')} className="btn-number btn-zero">
0
</button>
<button
onClick={() => {
const newValue = calculatorRef.current.appendNumber('.')
onChange(newValue)
}}
className="btn-number"
>
.
</button>
{/* = 버튼 */}
<button onClick={() => handleCompute(false)} className="btn-equals">
=
</button>
</div>
</div>
)}
</div>
)
},
)
CalculatorInput.displayName = 'CalculatorInput'