From fb20a1676b3e5e92c99fcba27dd12f15b64f39ba Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 11 Sep 2025 09:50:42 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=84=EC=82=B0=EA=B8=B0=EC=9D=98=20?= =?UTF-8?q?=EB=A7=88=EC=A7=80=EB=A7=89=20=EC=9E=85=EB=A0=A5=EC=88=AB?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EA=B2=8C..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/input/CalcInput.jsx | 798 +++++++++++----------- src/styles/calc.scss | 37 +- 2 files changed, 424 insertions(+), 411 deletions(-) diff --git a/src/components/common/input/CalcInput.jsx b/src/components/common/input/CalcInput.jsx index 311e71c3..3da3a7c5 100644 --- a/src/components/common/input/CalcInput.jsx +++ b/src/components/common/input/CalcInput.jsx @@ -2,433 +2,439 @@ 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) +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() + // 외부 ref와 내부 ref를 동기화 + useEffect(() => { + if (ref) { + if (typeof ref === 'function') { + ref(inputRef.current) + } else { + ref.current = inputRef.current } } - } + }, [ref]) - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, [value, onChange, hasOperation]) + // Sync displayValue with value prop + useEffect(() => { + setDisplayValue(value || '0') + }, [value]) - // 숫자 입력 처리 함수 수정 - 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 + // 클릭 외부 감지 + 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() + } + } } - 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 + + 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) - if (!hasOperation) { - onChange(calculator.currentOperand) + } 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.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) - } - }) - } - - // 연산자 처리 함수 수정 - 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.previousOperand = calculator.currentOperand || '0' calculator.operation = operation calculator.currentOperand = '' + setHasOperation(true) 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 + } + }) } - - // 포커스와 커서 위치 설정 (새로운 값의 길이로 설정) - requestAnimationFrame(() => { - if (inputRef.current) { - inputRef.current.focus() - const len = newDisplayValue.length - inputRef.current.setSelectionRange(len, len) - } - }) - } - // 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) - // Ensure focus is maintained - inputRef.current.focus() - } - }) - } - - // 계산 실행 함수 수정 - 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) - // Ensure focus is maintained - inputRef.current.focus() - } - }) - } - - // 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) { + // AC 버튼 클릭 핸들러 + const handleClear = () => { 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 + 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) - } - - onChange(filteredValue) - } - } + // 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 + } - // 키패드 토글 함수 - const toggleKeypad = (e) => { - if (e) { e.preventDefault() - e.stopPropagation() - } - const newShowKeypad = !showKeypad - setShowKeypad(newShowKeypad) - - // Show keypad 시에만 포커스 유지 - if (newShowKeypad) { - setTimeout(() => { - inputRef.current?.focus() - }, 0) - } - } + const calculator = calculatorRef.current + const { allowDecimal } = options - // 키보드 이벤트 처리 수정 - const handleKeyDown = (e) => { - if (readOnly) return + if (e.key === '.') { + // allowDecimal이 false이면 소수점 입력 무시 + if (!allowDecimal) 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 - } + // 소수점 입력 처리 + const currentValue = displayValue.toString() + const parts = currentValue.split(/[+\-×÷]/) + const lastPart = parts[parts.length - 1] - // 계산기 허용 키들이 입력되면 키패드 표시 (엔터키 제외) - 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('.')) { + // 이미 소수점이 있으면 무시 + if (!lastPart.includes('.')) { + handleNumber(e.key) + } + } else if (/^[0-9]$/.test(e.key)) { 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) // 엔터키로 호출됨을 표시 + } 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) - } - // 키패드가 숨겨진 상태에서 엔터키: 페이지로 전달 (preventDefault 하지 않음) - break - case 'Backspace': - case 'Delete': - handleDelete() - break + break - case 'Escape': - handleClear() - setShowKeypad(false) - break - - default: - break + default: + break + } } } - } - return ( -
- {label && ( - - )} - !readOnly && setShowKeypad(true)} - onFocus={() => !readOnly && setShowKeypad(true)} - onChange={handleInputChange} - onKeyDown={handleKeyDown} - tabIndex={readOnly ? -1 : 0} - placeholder={placeholder} - autoComplete={'off'} - /> + return ( +
+ {label && ( + + )} + !readOnly && setShowKeypad(true)} + onFocus={() => !readOnly && setShowKeypad(true)} + onChange={handleInputChange} + onKeyDown={handleKeyDown} + tabIndex={readOnly ? -1 : 0} + placeholder={placeholder} + autoComplete={'off'} + /> - {showKeypad && !readOnly && ( -
-
- - - - - - - - - {/* 숫자 버튼 */} - {[1,2,3,4,5,6,7,8,9].map((num) => ( - - ))} - {/* 0 버튼 */} - - - {/* = 버튼 */} - + + + + + + + + {/* 숫자 버튼 */} + {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => ( + + ))} + {/* 0 버튼 */} + + + {/* = 버튼 */} + +
-
- )} -
- ) -}) + )} + + ) + }, +) CalculatorInput.displayName = 'CalculatorInput' diff --git a/src/styles/calc.scss b/src/styles/calc.scss index f3977c65..c80355d0 100644 --- a/src/styles/calc.scss +++ b/src/styles/calc.scss @@ -1,17 +1,17 @@ // Variables $colors: ( - 'dark-700': #1f2937, - 'dark-600': #334155, - 'dark-500': #4b5563, - 'dark-400': #6b7280, - 'dark-300': #374151, - 'primary': #10b981, - 'primary-dark': #059669, - 'danger': #ef4444, - 'danger-dark': #dc2626, - 'warning': #f59e0b, - 'warning-dark': #d97706, - 'border': #475569 + 'dark-700': #1f2937, + 'dark-600': #334155, + 'dark-500': #4b5563, + 'dark-400': #6b7280, + 'dark-300': #374151, + 'primary': #10b981, + 'primary-dark': #059669, + 'danger': #ef4444, + 'danger-dark': #dc2626, + 'warning': #f59e0b, + 'warning-dark': #d97706, + 'border': #475569, ); // Mixins @@ -46,10 +46,15 @@ $colors: ( color: white; font-weight: bold; border-radius: 0.5rem; - text-align: right; + text-align: left; // 왼쪽 정렬 cursor: pointer; font-size: 0.625rem; box-sizing: border-box; + direction: ltr; // 왼쪽에서 오른쪽으로 입력 + white-space: nowrap; // 줄바꿈 방지 + // input 요소에서는 overflow 대신 다른 방법 사용 + text-overflow: clip; // 텍스트 잘림 처리 + unicode-bidi: bidi-override; // 텍스트 방향 강제 &:focus { outline: none; @@ -67,7 +72,9 @@ $colors: ( width: 120px; background-color: map-get($colors, 'dark-700'); border-radius: 0.5rem; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); padding: 0.5rem; z-index: 1000; animation: fadeIn 0.15s ease-out; @@ -153,4 +160,4 @@ $colors: ( padding: 0.5rem; font-size: 0.875rem; } -} \ No newline at end of file +}