2025-10-14 10:34:18 +09:00
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 계산기 위젯 컴포넌트
|
|
|
|
|
|
* - 기본 사칙연산 지원
|
|
|
|
|
|
* - 실시간 계산
|
|
|
|
|
|
* - 대시보드 위젯으로 사용 가능
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-10-17 12:04:40 +09:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
2025-10-14 10:34:18 +09:00
|
|
|
|
import { Button } from '@/components/ui/button';
|
2025-10-15 18:25:16 +09:00
|
|
|
|
import { DashboardElement } from '@/components/admin/dashboard/types';
|
2025-10-14 10:34:18 +09:00
|
|
|
|
|
|
|
|
|
|
interface CalculatorWidgetProps {
|
2025-10-15 18:25:16 +09:00
|
|
|
|
element?: DashboardElement;
|
2025-10-14 10:34:18 +09:00
|
|
|
|
className?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 18:25:16 +09:00
|
|
|
|
export default function CalculatorWidget({ element, className = '' }: CalculatorWidgetProps) {
|
2025-10-14 10:34:18 +09:00
|
|
|
|
const [display, setDisplay] = useState<string>('0');
|
|
|
|
|
|
const [previousValue, setPreviousValue] = useState<number | null>(null);
|
|
|
|
|
|
const [operation, setOperation] = useState<string | null>(null);
|
|
|
|
|
|
const [waitingForOperand, setWaitingForOperand] = useState<boolean>(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 숫자 입력 처리
|
|
|
|
|
|
const handleNumber = (num: string) => {
|
|
|
|
|
|
if (waitingForOperand) {
|
|
|
|
|
|
setDisplay(num);
|
|
|
|
|
|
setWaitingForOperand(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setDisplay(display === '0' ? num : display + num);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 소수점 입력
|
|
|
|
|
|
const handleDecimal = () => {
|
|
|
|
|
|
if (waitingForOperand) {
|
|
|
|
|
|
setDisplay('0.');
|
|
|
|
|
|
setWaitingForOperand(false);
|
|
|
|
|
|
} else if (display.indexOf('.') === -1) {
|
|
|
|
|
|
setDisplay(display + '.');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 연산자 입력
|
|
|
|
|
|
const handleOperation = (nextOperation: string) => {
|
|
|
|
|
|
const inputValue = parseFloat(display);
|
|
|
|
|
|
|
|
|
|
|
|
if (previousValue === null) {
|
|
|
|
|
|
setPreviousValue(inputValue);
|
|
|
|
|
|
} else if (operation) {
|
|
|
|
|
|
const currentValue = previousValue || 0;
|
|
|
|
|
|
const newValue = calculate(currentValue, inputValue, operation);
|
|
|
|
|
|
|
|
|
|
|
|
setDisplay(String(newValue));
|
|
|
|
|
|
setPreviousValue(newValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setWaitingForOperand(true);
|
|
|
|
|
|
setOperation(nextOperation);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 계산 수행
|
|
|
|
|
|
const calculate = (firstValue: number, secondValue: number, operation: string): number => {
|
|
|
|
|
|
switch (operation) {
|
|
|
|
|
|
case '+':
|
|
|
|
|
|
return firstValue + secondValue;
|
|
|
|
|
|
case '-':
|
|
|
|
|
|
return firstValue - secondValue;
|
|
|
|
|
|
case '×':
|
|
|
|
|
|
return firstValue * secondValue;
|
|
|
|
|
|
case '÷':
|
|
|
|
|
|
return secondValue !== 0 ? firstValue / secondValue : 0;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return secondValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 등호 처리
|
|
|
|
|
|
const handleEquals = () => {
|
|
|
|
|
|
const inputValue = parseFloat(display);
|
|
|
|
|
|
|
|
|
|
|
|
if (previousValue !== null && operation) {
|
|
|
|
|
|
const newValue = calculate(previousValue, inputValue, operation);
|
|
|
|
|
|
setDisplay(String(newValue));
|
|
|
|
|
|
setPreviousValue(null);
|
|
|
|
|
|
setOperation(null);
|
|
|
|
|
|
setWaitingForOperand(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 초기화
|
|
|
|
|
|
const handleClear = () => {
|
|
|
|
|
|
setDisplay('0');
|
|
|
|
|
|
setPreviousValue(null);
|
|
|
|
|
|
setOperation(null);
|
|
|
|
|
|
setWaitingForOperand(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 백스페이스
|
|
|
|
|
|
const handleBackspace = () => {
|
|
|
|
|
|
if (!waitingForOperand) {
|
|
|
|
|
|
const newDisplay = display.slice(0, -1);
|
|
|
|
|
|
setDisplay(newDisplay || '0');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 부호 변경
|
|
|
|
|
|
const handleSign = () => {
|
|
|
|
|
|
const value = parseFloat(display);
|
|
|
|
|
|
setDisplay(String(value * -1));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 퍼센트
|
|
|
|
|
|
const handlePercent = () => {
|
|
|
|
|
|
const value = parseFloat(display);
|
|
|
|
|
|
setDisplay(String(value / 100));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-17 12:04:40 +09:00
|
|
|
|
// 키보드 입력 처리
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
|
const key = event.key;
|
|
|
|
|
|
|
|
|
|
|
|
// 숫자 키 (0-9)
|
|
|
|
|
|
if (/^[0-9]$/.test(key)) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleNumber(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 연산자 키
|
|
|
|
|
|
else if (key === '+' || key === '-' || key === '*' || key === '/') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleOperation(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 소수점
|
|
|
|
|
|
else if (key === '.') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleDecimal();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Enter 또는 = (계산)
|
|
|
|
|
|
else if (key === 'Enter' || key === '=') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleEquals();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Escape 또는 c (초기화)
|
|
|
|
|
|
else if (key === 'Escape' || key.toLowerCase() === 'c') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleClear();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Backspace (지우기)
|
|
|
|
|
|
else if (key === 'Backspace') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleBackspace();
|
|
|
|
|
|
}
|
|
|
|
|
|
// % (퍼센트)
|
|
|
|
|
|
else if (key === '%') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handlePercent();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 이벤트 리스너 등록
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
|
|
|
|
|
|
|
// 클린업
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [display, previousValue, operation, waitingForOperand]);
|
|
|
|
|
|
|
2025-10-14 10:34:18 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className={`h-full w-full p-3 bg-gradient-to-br from-slate-50 to-gray-100 ${className}`}>
|
2025-10-15 18:25:16 +09:00
|
|
|
|
<div className="h-full flex flex-col gap-2">
|
|
|
|
|
|
{/* 제목 */}
|
2025-10-17 14:52:08 +09:00
|
|
|
|
<h3 className="text-base font-semibold text-gray-900 text-center">{element?.customTitle || "계산기"}</h3>
|
2025-10-15 18:25:16 +09:00
|
|
|
|
|
2025-10-14 10:34:18 +09:00
|
|
|
|
{/* 디스플레이 */}
|
|
|
|
|
|
<div className="bg-white border-2 border-gray-200 rounded-lg p-4 shadow-inner min-h-[80px]">
|
|
|
|
|
|
<div className="text-right h-full flex flex-col justify-center">
|
|
|
|
|
|
<div className="h-4 mb-1">
|
|
|
|
|
|
{operation && previousValue !== null && (
|
|
|
|
|
|
<div className="text-xs text-gray-400">
|
|
|
|
|
|
{previousValue} {operation}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-2xl font-bold text-gray-900 truncate">
|
|
|
|
|
|
{display}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 버튼 그리드 */}
|
|
|
|
|
|
<div className="flex-1 grid grid-cols-4 gap-2">
|
|
|
|
|
|
{/* 첫 번째 줄 */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={handleClear}
|
|
|
|
|
|
className="h-full text-red-600 hover:bg-red-50 hover:text-red-700 font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
AC
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={handleSign}
|
|
|
|
|
|
className="h-full text-gray-600 hover:bg-gray-100 font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
+/-
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={handlePercent}
|
|
|
|
|
|
className="h-full text-gray-600 hover:bg-gray-100 font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
%
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
onClick={() => handleOperation('÷')}
|
|
|
|
|
|
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
÷
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 두 번째 줄 */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('7')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
7
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('8')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
8
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('9')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
9
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
onClick={() => handleOperation('×')}
|
|
|
|
|
|
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 세 번째 줄 */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('4')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
4
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('5')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
5
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('6')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
6
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
onClick={() => handleOperation('-')}
|
|
|
|
|
|
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
-
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 네 번째 줄 */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('1')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
1
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('2')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
2
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('3')}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
3
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
onClick={() => handleOperation('+')}
|
|
|
|
|
|
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
+
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 다섯 번째 줄 */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleNumber('0')}
|
|
|
|
|
|
className="h-full col-span-2 hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
0
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={handleDecimal}
|
|
|
|
|
|
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
.
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
onClick={handleEquals}
|
|
|
|
|
|
className="h-full bg-green-500 hover:bg-green-600 text-white font-semibold select-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
=
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|