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 }, 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 = '' if (hasOperation) { // 연산자 이후 숫자 입력 시 if (calculator.currentOperand === '0' || calculator.shouldResetDisplay) { calculator.currentOperand = num.toString() calculator.shouldResetDisplay = false } else { calculator.currentOperand = (calculator.currentOperand || '') + num } newDisplayValue = calculator.previousOperand + calculator.operation + calculator.currentOperand setDisplayValue(newDisplayValue) } else { // 첫 번째 숫자 입력 시 if (displayValue === '0' || calculator.shouldResetDisplay) { calculator.currentOperand = num.toString() calculator.shouldResetDisplay = false newDisplayValue = calculator.currentOperand setDisplayValue(newDisplayValue) if (!hasOperation) { onChange(calculator.currentOperand) } } 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 (