Compare commits

..

No commits in common. "32c38b6600b794f65aa3dc2769cca7b470dd0662" and "82fac578adce7a8ef2d39ea940a05c73d6690716" have entirely different histories.

3 changed files with 414 additions and 427 deletions

View File

@ -2,439 +2,433 @@ import React, { useState, useRef, useEffect, forwardRef } from 'react'
import { createCalculator } from '@/util/calc-utils' import { createCalculator } from '@/util/calc-utils'
import '@/styles/calc.scss' import '@/styles/calc.scss'
export const CalculatorInput = forwardRef( export const CalculatorInput = forwardRef(({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder}, ref) => {
({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder }, ref) => { const [showKeypad, setShowKeypad] = useState(false)
const [showKeypad, setShowKeypad] = useState(false) const [displayValue, setDisplayValue] = useState(value || '0')
const [displayValue, setDisplayValue] = useState(value || '0') const [hasOperation, setHasOperation] = useState(false)
const [hasOperation, setHasOperation] = useState(false) const calculatorRef = useRef(createCalculator(options))
const calculatorRef = useRef(createCalculator(options)) const containerRef = useRef(null)
const containerRef = useRef(null) const inputRef = useRef(null)
const inputRef = useRef(null)
// ref ref // ref ref
useEffect(() => { useEffect(() => {
if (ref) { if (ref) {
if (typeof ref === 'function') { if (typeof ref === 'function') {
ref(inputRef.current) 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 { } else {
// ref.current = inputRef.current
if (displayValue === '0' || calculator.shouldResetDisplay) { }
calculator.currentOperand = num.toString() }
calculator.shouldResetDisplay = false }, [ref])
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue) // Sync displayValue with value prop
if (!hasOperation) { useEffect(() => {
onChange(calculator.currentOperand) setDisplayValue(value || '0')
} }, [value])
} else {
calculator.currentOperand = (calculator.currentOperand || '') + num //
newDisplayValue = calculator.currentOperand useEffect(() => {
setDisplayValue(newDisplayValue) const handleClickOutside = (event) => {
if (!hasOperation) { if (containerRef.current && !containerRef.current.contains(event.target)) {
onChange(newDisplayValue) setShowKeypad(false)
} if (hasOperation) {
// If there's an operation in progress, compute the result when losing focus
handleCompute()
} }
} }
//
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = newDisplayValue.length
inputRef.current.setSelectionRange(len, len)
//
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
} }
// document.addEventListener('mousedown', handleClickOutside)
const handleOperation = (operation) => { return () => document.removeEventListener('mousedown', handleClickOutside)
const calculator = calculatorRef.current }, [value, onChange, hasOperation])
let newDisplayValue = ''
// ( ) //
if (!calculator.currentOperand && calculator.previousOperand) { const handleNumber = (num) => {
calculator.operation = operation const calculator = calculatorRef.current
newDisplayValue = calculator.previousOperand + operation 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) setDisplayValue(newDisplayValue)
setHasOperation(true) if (!hasOperation) {
} else if (hasOperation) { onChange(calculator.currentOperand)
// ,
const result = calculator.compute()
if (result !== undefined) {
calculator.previousOperand = result.toString()
calculator.operation = operation
calculator.currentOperand = ''
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
} }
} else { } else {
// calculator.currentOperand = (calculator.currentOperand || '') + num
calculator.previousOperand = calculator.currentOperand || '0' newDisplayValue = calculator.currentOperand
calculator.operation = operation
calculator.currentOperand = ''
setHasOperation(true)
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue) 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
}
})
} }
// AC // ( )
const handleClear = () => { requestAnimationFrame(() => {
const calculator = calculatorRef.current if (inputRef.current) {
const newValue = calculator.clear() inputRef.current.focus()
const displayValue = newValue || '0' const len = newDisplayValue.length
setDisplayValue(displayValue) inputRef.current.setSelectionRange(len, len)
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 handleOperation = (operation) => {
const calculator = calculatorRef.current const calculator = calculatorRef.current
if (!hasOperation || !calculator.currentOperand) return 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() const result = calculator.compute()
if (result !== undefined) { if (result !== undefined) {
const resultStr = result.toString() calculator.previousOperand = result.toString()
setDisplayValue(resultStr) calculator.operation = operation
setHasOperation(false) calculator.currentOperand = ''
// Only call onChange with the final result newDisplayValue = calculator.previousOperand + operation
onChange(resultStr) setDisplayValue(newDisplayValue)
//
if (!fromEnterKey) {
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = resultStr.length
inputRef.current.setSelectionRange(len, len)
}
})
}
} }
} else {
//
calculator.previousOperand = calculator.currentOperand || '0'
calculator.operation = operation
calculator.currentOperand = ''
setHasOperation(true)
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
} }
// DEL // ( )
const handleDelete = () => { 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) {
const calculator = calculatorRef.current const calculator = calculatorRef.current
const newValue = calculator.deleteNumber() const hasOperation = /[+\-×÷]/.test(filteredValue)
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 - if (hasOperation) {
const handleInputChange = (e) => { const [operand1, operator, operand2] = filteredValue.split(/([+\-×÷])/)
if (readOnly) return calculator.previousOperand = operand1 || ''
calculator.operation = operator || ''
const inputValue = e.target.value calculator.currentOperand = operand2 || ''
setHasOperation(true)
// (, , )
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 { } else {
switch (e.key) { calculator.currentOperand = filteredValue
case '+': setHasOperation(false)
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
}
} }
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
} }
return ( //
<div ref={containerRef} className="calculator-input-wrapper"> if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
{label && ( setShowKeypad(true)
<label htmlFor={id} className="calculator-label"> return
{label} }
</label>
)}
<input
ref={inputRef}
type="text"
id={id}
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'}
/>
{showKeypad && !readOnly && ( // ( )
<div className="keypad-container"> if (/^[0-9+\-×÷.=]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete' || e.key === '*' || e.key === '/') {
<div className="keypad-grid"> setShowKeypad(true)
<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"> //
× if (!showKeypad && e.key === 'Enter') {
</button> return
<button onClick={() => handleOperation('-')} className="btn-operator"> }
-
</button>
<button onClick={() => handleOperation('+')} className="btn-operator">
+
</button>
{/* 숫자 버튼 */} e.preventDefault()
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => ( const calculator = calculatorRef.current
<button key={num} onClick={() => handleNumber(num)} className="btn-number"> const { allowDecimal } = options
{num}
</button> if (e.key === '.') {
))} // allowDecimal false
{/* 0 버튼 */} if (!allowDecimal) return
<button onClick={() => handleNumber('0')} className="btn-number btn-zero">
0 //
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}
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'}
/>
{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> </button>
<button ))}
onClick={() => { {/* 0 버튼 */}
const newValue = calculatorRef.current.appendNumber('.') <button onClick={() => handleNumber('0')} className="btn-number btn-zero">
onChange(newValue) 0
}} </button>
className="btn-number" <button
> onClick={() => {
. const newValue = calculatorRef.current.appendNumber('.')
</button> onChange(newValue)
{/* = 버튼 */} }}
<button onClick={() => handleCompute(false)} className="btn-equals"> className="btn-number"
= >
</button> .
</div> </button>
{/* = 버튼 */}
<button onClick={() => handleCompute(false)} className="btn-equals">
=
</button>
</div> </div>
)} </div>
</div> )}
) </div>
}, )
) })
CalculatorInput.displayName = 'CalculatorInput' CalculatorInput.displayName = 'CalculatorInput'

View File

@ -213,7 +213,7 @@ export function useEvent() {
const modulePoints = [] const modulePoints = []
const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE) const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
modules.forEach((module) => { modules.forEach((module) => {
module.getCurrentPoints().forEach((point) => { module.points.forEach((point) => {
modulePoints.push({ x: point.x, y: point.y }) modulePoints.push({ x: point.x, y: point.y })
}) })
}) })

View File

@ -1,17 +1,17 @@
// Variables // Variables
$colors: ( $colors: (
'dark-700': #1f2937, 'dark-700': #1f2937,
'dark-600': #334155, 'dark-600': #334155,
'dark-500': #4b5563, 'dark-500': #4b5563,
'dark-400': #6b7280, 'dark-400': #6b7280,
'dark-300': #374151, 'dark-300': #374151,
'primary': #10b981, 'primary': #10b981,
'primary-dark': #059669, 'primary-dark': #059669,
'danger': #ef4444, 'danger': #ef4444,
'danger-dark': #dc2626, 'danger-dark': #dc2626,
'warning': #f59e0b, 'warning': #f59e0b,
'warning-dark': #d97706, 'warning-dark': #d97706,
'border': #475569, 'border': #475569
); );
// Mixins // Mixins
@ -46,15 +46,10 @@ $colors: (
color: white; color: white;
font-weight: bold; font-weight: bold;
border-radius: 0.5rem; border-radius: 0.5rem;
text-align: left; // 왼쪽 정렬 text-align: right;
cursor: pointer; cursor: pointer;
font-size: 0.625rem; font-size: 0.625rem;
box-sizing: border-box; box-sizing: border-box;
direction: ltr; // 왼쪽에서 오른쪽으로 입력
white-space: nowrap; // 줄바꿈 방지
// input 요소에서는 overflow 대신 다른 방법 사용
text-overflow: clip; // 텍스트 잘림 처리
unicode-bidi: bidi-override; // 텍스트 방향 강제
&:focus { &:focus {
outline: none; outline: none;
@ -72,9 +67,7 @@ $colors: (
width: 120px; width: 120px;
background-color: map-get($colors, 'dark-700'); background-color: map-get($colors, 'dark-700');
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
padding: 0.5rem; padding: 0.5rem;
z-index: 1000; z-index: 1000;
animation: fadeIn 0.15s ease-out; animation: fadeIn 0.15s ease-out;