메인 받은거랑 계산기 위젯, 배경색
This commit is contained in:
parent
97902d2a49
commit
85c561c8b5
|
|
@ -17,6 +17,11 @@ const ExchangeWidget = dynamic(() => import("@/components/dashboard/widgets/Exch
|
|||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const CalculatorWidget = dynamic(() => import("@/components/dashboard/widgets/CalculatorWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 시계 위젯 임포트
|
||||
import { ClockWidget } from "./widgets/ClockWidget";
|
||||
|
||||
|
|
@ -376,6 +381,11 @@ export function CanvasElement({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "calculator" ? (
|
||||
// 계산기 위젯 렌더링
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<CalculatorWidget />
|
||||
</div>
|
||||
) : (
|
||||
// 기타 위젯 렌더링
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface DashboardCanvasProps {
|
|||
onRemoveElement: (id: string) => void;
|
||||
onSelectElement: (id: string | null) => void;
|
||||
onConfigureElement?: (element: DashboardElement) => void;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +33,7 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
onRemoveElement,
|
||||
onSelectElement,
|
||||
onConfigureElement,
|
||||
backgroundColor = '#f9fafb',
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
|
@ -104,8 +106,9 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`relative rounded-lg bg-gray-50 shadow-inner ${isDragOver ? "bg-blue-50/50" : ""} `}
|
||||
className={`relative rounded-lg shadow-inner ${isDragOver ? "bg-blue-50/50" : ""} `}
|
||||
style={{
|
||||
backgroundColor,
|
||||
width: `${GRID_CONFIG.CANVAS_WIDTH}px`,
|
||||
minHeight: `${minCanvasHeight}px`,
|
||||
// 12 컬럼 그리드 배경
|
||||
|
|
|
|||
|
|
@ -291,6 +291,8 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string {
|
|||
return "☁️ 날씨 위젯";
|
||||
case "clock":
|
||||
return "⏰ 시계 위젯";
|
||||
case "calculator":
|
||||
return "🧮 계산기 위젯";
|
||||
default:
|
||||
return "🔧 위젯";
|
||||
}
|
||||
|
|
@ -319,6 +321,8 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string {
|
|||
return "서울\n23°C\n구름 많음";
|
||||
case "clock":
|
||||
return "clock";
|
||||
case "calculator":
|
||||
return "calculator";
|
||||
default:
|
||||
return "위젯 내용이 여기에 표시됩니다";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,15 @@ export function DashboardSidebar() {
|
|||
type="widget"
|
||||
subtype="weather"
|
||||
onDragStart={handleDragStart}
|
||||
className="border-l-4 border-orange-500"
|
||||
className="border-l-4 border-cyan-500"
|
||||
/>
|
||||
<DraggableItem
|
||||
icon="🧮"
|
||||
title="계산기 위젯"
|
||||
type="widget"
|
||||
subtype="calculator"
|
||||
onDragStart={handleDragStart}
|
||||
className="border-l-4 border-green-500"
|
||||
/>
|
||||
<DraggableItem
|
||||
icon="⏰"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface DashboardToolbarProps {
|
||||
onClearCanvas: () => void;
|
||||
onSaveLayout: () => void;
|
||||
canvasBackgroundColor: string;
|
||||
onCanvasBackgroundColorChange: (color: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대시보드 툴바 컴포넌트
|
||||
* - 전체 삭제, 레이아웃 저장 등 주요 액션 버튼
|
||||
*/
|
||||
export function DashboardToolbar({ onClearCanvas, onSaveLayout }: DashboardToolbarProps) {
|
||||
export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackgroundColor, onCanvasBackgroundColorChange }: DashboardToolbarProps) {
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
return (
|
||||
<div className="absolute top-5 left-5 bg-white p-3 rounded-lg shadow-lg z-50 flex gap-3">
|
||||
<button
|
||||
|
|
@ -37,6 +40,71 @@ export function DashboardToolbar({ onClearCanvas, onSaveLayout }: DashboardToolb
|
|||
>
|
||||
💾 레이아웃 저장
|
||||
</button>
|
||||
|
||||
{/* 캔버스 배경색 변경 버튼 */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowColorPicker(!showColorPicker)}
|
||||
className="
|
||||
px-4 py-2 border border-gray-300 bg-white rounded-md
|
||||
text-sm font-medium text-gray-700
|
||||
hover:bg-gray-50 hover:border-gray-400
|
||||
transition-colors duration-200
|
||||
flex items-center gap-2
|
||||
"
|
||||
>
|
||||
🎨 캔버스 색상
|
||||
<div
|
||||
className="w-4 h-4 rounded border border-gray-300"
|
||||
style={{ backgroundColor: canvasBackgroundColor }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* 색상 선택 패널 */}
|
||||
{showColorPicker && (
|
||||
<div className="absolute top-full left-0 mt-2 bg-white p-4 rounded-lg shadow-xl z-50 border border-gray-200 w-[280px]">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<input
|
||||
type="color"
|
||||
value={canvasBackgroundColor}
|
||||
onChange={(e) => onCanvasBackgroundColorChange(e.target.value)}
|
||||
className="h-10 w-16 border border-gray-300 rounded cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={canvasBackgroundColor}
|
||||
onChange={(e) => onCanvasBackgroundColorChange(e.target.value)}
|
||||
placeholder="#ffffff"
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 프리셋 색상 */}
|
||||
<div className="grid grid-cols-6 gap-2 mb-3">
|
||||
{[
|
||||
'#ffffff', '#f9fafb', '#f3f4f6', '#e5e7eb',
|
||||
'#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b',
|
||||
'#10b981', '#06b6d4', '#6366f1', '#84cc16',
|
||||
].map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => onCanvasBackgroundColorChange(color)}
|
||||
className={`h-8 rounded border-2 ${canvasBackgroundColor === color ? 'border-blue-500 ring-2 ring-blue-200' : 'border-gray-300'}`}
|
||||
style={{ backgroundColor: color }}
|
||||
title={color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowColorPicker(false)}
|
||||
className="w-full px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export type ElementSubtype =
|
|||
| "combo" // 차트 타입
|
||||
| "exchange"
|
||||
| "weather"
|
||||
| "clock"; // 위젯 타입
|
||||
| "clock"
|
||||
| "calculator"; // 위젯 타입
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,286 @@
|
|||
'use client';
|
||||
|
||||
/**
|
||||
* 계산기 위젯 컴포넌트
|
||||
* - 기본 사칙연산 지원
|
||||
* - 실시간 계산
|
||||
* - 대시보드 위젯으로 사용 가능
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface CalculatorWidgetProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function CalculatorWidget({ 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));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`h-full w-full p-3 bg-gradient-to-br from-slate-50 to-gray-100 ${className}`}>
|
||||
<div className="h-full flex flex-col justify-center gap-2">
|
||||
{/* 디스플레이 */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue