342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
"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>
|
||
);
|
||
}
|