ERP-node/frontend/components/dashboard/widgets/CalculatorWidget.tsx

343 lines
10 KiB
TypeScript
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.

'use client';
/**
* 계산기 위젯 컴포넌트
* - 기본 사칙연산 지원
* - 실시간 계산
* - 대시보드 위젯으로 사용 가능
*/
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { DashboardElement } from '@/components/admin/dashboard/types';
interface CalculatorWidgetProps {
element?: DashboardElement;
className?: string;
}
export default function CalculatorWidget({ element, className = '' }: CalculatorWidgetProps) {
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));
};
// 키보드 입력 처리
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]);
return (
<div className={`h-full w-full p-2 sm:p-3 bg-background ${className}`}>
<div className="h-full flex flex-col gap-1.5 sm:gap-2">
{/* 제목 */}
<h3 className="text-sm sm:text-base font-semibold text-foreground text-center">{element?.customTitle || "계산기"}</h3>
{/* 디스플레이 */}
<div className="bg-background border-2 border-border rounded-lg p-2 sm:p-4 shadow-inner min-h-[60px] sm:min-h-[80px]">
<div className="text-right h-full flex flex-col justify-center">
<div className="h-3 sm:h-4 mb-0.5 sm:mb-1">
{operation && previousValue !== null && (
<div className="text-[10px] sm:text-xs text-muted-foreground">
{previousValue} {operation}
</div>
)}
</div>
<div className="text-lg sm:text-2xl font-bold text-foreground truncate">
{display}
</div>
</div>
</div>
{/* 버튼 그리드 */}
<div className="flex-1 grid grid-cols-4 gap-1 sm:gap-2">
{/* 첫 번째 줄 */}
<Button
variant="outline"
onClick={handleClear}
className="h-full text-xs sm:text-base text-destructive hover:bg-destructive/10 hover:text-destructive font-semibold select-none"
>
AC
</Button>
<Button
variant="outline"
onClick={handleSign}
className="h-full text-xs sm:text-base text-foreground hover:bg-muted font-semibold select-none"
>
+/-
</Button>
<Button
variant="outline"
onClick={handlePercent}
className="h-full text-xs sm:text-base text-foreground hover:bg-muted font-semibold select-none"
>
%
</Button>
<Button
variant="default"
onClick={() => handleOperation('÷')}
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-primary-foreground font-semibold select-none"
>
÷
</Button>
{/* 두 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber('7')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
7
</Button>
<Button
variant="outline"
onClick={() => handleNumber('8')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
8
</Button>
<Button
variant="outline"
onClick={() => handleNumber('9')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
9
</Button>
<Button
variant="default"
onClick={() => handleOperation('×')}
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-primary-foreground font-semibold select-none"
>
×
</Button>
{/* 세 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber('4')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
4
</Button>
<Button
variant="outline"
onClick={() => handleNumber('5')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
5
</Button>
<Button
variant="outline"
onClick={() => handleNumber('6')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
6
</Button>
<Button
variant="default"
onClick={() => handleOperation('-')}
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-primary-foreground font-semibold select-none"
>
-
</Button>
{/* 네 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber('1')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
1
</Button>
<Button
variant="outline"
onClick={() => handleNumber('2')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
2
</Button>
<Button
variant="outline"
onClick={() => handleNumber('3')}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
3
</Button>
<Button
variant="default"
onClick={() => handleOperation('+')}
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-primary-foreground font-semibold select-none"
>
+
</Button>
{/* 다섯 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber('0')}
className="h-full col-span-2 text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
0
</Button>
<Button
variant="outline"
onClick={handleDecimal}
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
>
.
</Button>
<Button
variant="default"
onClick={handleEquals}
className="h-full text-xs sm:text-base bg-success hover:bg-success/90 text-primary-foreground font-semibold select-none"
>
=
</Button>
</div>
</div>
</div>
);
}