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

342 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={`bg-background h-full w-full p-2 sm:p-3 ${className}`}>
<div className="flex h-full flex-col gap-1.5 sm:gap-2">
{/* 제목 */}
<h3 className="text-foreground text-center text-sm font-semibold sm:text-base">
{element?.customTitle || "계산기"}
</h3>
{/* 디스플레이 */}
<div className="bg-background border-border min-h-[60px] rounded-lg border-2 p-2 shadow-inner sm:min-h-[80px] sm:p-4">
<div className="flex h-full flex-col justify-center text-right">
<div className="mb-0.5 h-3 sm:mb-1 sm:h-4">
{operation && previousValue !== null && (
<div className="text-muted-foreground text-[10px] sm:text-xs">
{previousValue} {operation}
</div>
)}
</div>
<div className="text-foreground truncate text-lg font-bold sm:text-2xl">{display}</div>
</div>
</div>
{/* 버튼 그리드 */}
<div className="grid flex-1 grid-cols-4 gap-1 sm:gap-2">
{/* 첫 번째 줄 */}
<Button
variant="outline"
onClick={handleClear}
className="text-destructive hover:bg-destructive/10 hover:text-destructive h-full text-xs font-semibold select-none sm:text-base"
>
AC
</Button>
<Button
variant="outline"
onClick={handleSign}
className="text-foreground hover:bg-muted h-full text-xs font-semibold select-none sm:text-base"
>
+/-
</Button>
<Button
variant="outline"
onClick={handlePercent}
className="text-foreground hover:bg-muted h-full text-xs font-semibold select-none sm:text-base"
>
%
</Button>
<Button
variant="default"
onClick={() => handleOperation("÷")}
className="bg-primary hover:bg-primary/90 text-primary-foreground h-full text-xs font-semibold select-none sm:text-base"
>
÷
</Button>
{/* 두 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber("7")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
7
</Button>
<Button
variant="outline"
onClick={() => handleNumber("8")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
8
</Button>
<Button
variant="outline"
onClick={() => handleNumber("9")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
9
</Button>
<Button
variant="default"
onClick={() => handleOperation("×")}
className="bg-primary hover:bg-primary/90 text-primary-foreground h-full text-xs font-semibold select-none sm:text-base"
>
×
</Button>
{/* 세 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber("4")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
4
</Button>
<Button
variant="outline"
onClick={() => handleNumber("5")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
5
</Button>
<Button
variant="outline"
onClick={() => handleNumber("6")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
6
</Button>
<Button
variant="default"
onClick={() => handleOperation("-")}
className="bg-primary hover:bg-primary/90 text-primary-foreground h-full text-xs font-semibold select-none sm:text-base"
>
-
</Button>
{/* 네 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber("1")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
1
</Button>
<Button
variant="outline"
onClick={() => handleNumber("2")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
2
</Button>
<Button
variant="outline"
onClick={() => handleNumber("3")}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
3
</Button>
<Button
variant="default"
onClick={() => handleOperation("+")}
className="bg-primary hover:bg-primary/90 text-primary-foreground h-full text-xs font-semibold select-none sm:text-base"
>
+
</Button>
{/* 다섯 번째 줄 */}
<Button
variant="outline"
onClick={() => handleNumber("0")}
className="hover:bg-muted col-span-2 h-full text-sm font-semibold select-none sm:text-lg"
>
0
</Button>
<Button
variant="outline"
onClick={handleDecimal}
className="hover:bg-muted h-full text-sm font-semibold select-none sm:text-lg"
>
.
</Button>
<Button
variant="default"
onClick={handleEquals}
className="bg-success hover:bg-success/90 text-primary-foreground h-full text-xs font-semibold select-none sm:text-base"
>
=
</Button>
</div>
</div>
</div>
);
}