diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 746e4d54..9fc4974a 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -70,6 +70,11 @@ export function CanvasElement({ return; } + // 위젯 내부 (헤더 제외) 클릭 시 드래그 무시 - 인터랙티브 사용 가능 + if ((e.target as HTMLElement).closest(".widget-interactive-area")) { + return; + } + onSelect(element.id); setIsDragging(true); setDragStart({ @@ -344,12 +349,12 @@ export function CanvasElement({ ) : element.type === "widget" && element.subtype === "weather" ? ( // 날씨 위젯 렌더링 -
+
) : element.type === "widget" && element.subtype === "exchange" ? ( // 환율 위젯 렌더링 -
+
(null); const [lastUpdated, setLastUpdated] = useState(null); + const [calculatorAmount, setCalculatorAmount] = useState(''); + const [displayAmount, setDisplayAmount] = useState(''); // 지원 통화 목록 const currencies = [ @@ -86,6 +89,33 @@ export default function ExchangeWidget({ return currencies.find((c) => c.value === currency)?.symbol || currency; }; + // 계산기 금액 입력 처리 + const handleCalculatorInput = (e: React.ChangeEvent) => { + const value = e.target.value; + + // 쉼표 제거 후 숫자만 추출 + const cleanValue = value.replace(/,/g, '').replace(/[^\d]/g, ''); + + // 계산용 원본 값 저장 + setCalculatorAmount(cleanValue); + + // 표시용 포맷팅된 값 저장 + if (cleanValue === '') { + setDisplayAmount(''); + } else { + const num = parseInt(cleanValue); + setDisplayAmount(num.toLocaleString('ko-KR')); + } + }; + + // 계산 결과 + const calculateResult = () => { + const amount = parseFloat(calculatorAmount || '0'); + if (!exchangeRate || isNaN(amount)) return 0; + + return amount * (base === 'KRW' ? exchangeRate.rate : 1 / exchangeRate.rate); + }; + // 로딩 상태 if (loading && !exchangeRate) { return ( @@ -98,31 +128,15 @@ export default function ExchangeWidget({ ); } - // 에러 상태 - if (error || !exchangeRate) { - return ( -
- -

{error || '환율 정보를 불러올 수 없습니다.'}

- -
- ); - } + // 에러 상태 - 하지만 계산기는 표시 + const hasError = error || !exchangeRate; return ( -
+
{/* 헤더 */} -
+
-

💱 환율

+

💱 환율

{lastUpdated ? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', { @@ -144,9 +158,9 @@ export default function ExchangeWidget({

{/* 통화 선택 */} -
+
- + @@ -181,54 +195,78 @@ export default function ExchangeWidget({
+ {/* 에러 메시지 */} + {hasError && ( +
+

{error || '환율 정보를 불러올 수 없습니다.'}

+ +
+ )} + {/* 환율 표시 */} -
-
-
- {exchangeRate.base === 'KRW' ? '1,000' : '1'} {getCurrencySymbol(exchangeRate.base)} = + {!hasError && ( +
+
+
+ {exchangeRate.base === 'KRW' ? '1,000' : '1'} {getCurrencySymbol(exchangeRate.base)} = +
+
+ {exchangeRate.base === 'KRW' + ? (exchangeRate.rate * 1000).toLocaleString('ko-KR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + : exchangeRate.rate.toLocaleString('ko-KR', { + minimumFractionDigits: 2, + maximumFractionDigits: 4, + })} +
+
{getCurrencySymbol(exchangeRate.target)}
-
- {exchangeRate.base === 'KRW' - ? (exchangeRate.rate * 1000).toLocaleString('ko-KR', { - minimumFractionDigits: 2, +
+ )} + + {/* 계산기 입력 */} +
+
+
+ + {base} +
+ +
+
+ +
+
+ +
+
+ {calculateResult().toLocaleString('ko-KR', { + minimumFractionDigits: 0, maximumFractionDigits: 2, - }) - : exchangeRate.rate.toLocaleString('ko-KR', { - minimumFractionDigits: 2, - maximumFractionDigits: 4, })} -
-
{getCurrencySymbol(exchangeRate.target)}
-
-
- - {/* 계산 예시 */} -
-
-
10,000 {base}
-
- {(10000 * (base === 'KRW' ? exchangeRate.rate : 1 / exchangeRate.rate)).toLocaleString('ko-KR', { - minimumFractionDigits: 0, - maximumFractionDigits: 2, - })}{' '} - {target} +
+ {target} +
-
-
100,000 {base}
-
- {(100000 * (base === 'KRW' ? exchangeRate.rate : 1 / exchangeRate.rate)).toLocaleString('ko-KR', { - minimumFractionDigits: 0, - maximumFractionDigits: 2, - })}{' '} - {target} -
-
-
- {/* 데이터 출처 */} -
+

출처: {exchangeRate.source}

diff --git a/frontend/components/dashboard/widgets/WeatherWidget.tsx b/frontend/components/dashboard/widgets/WeatherWidget.tsx index ef195aaa..19753387 100644 --- a/frontend/components/dashboard/widgets/WeatherWidget.tsx +++ b/frontend/components/dashboard/widgets/WeatherWidget.tsx @@ -18,6 +18,7 @@ import { RefreshCw, Check, ChevronsUpDown, + Settings, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; @@ -34,11 +35,37 @@ export default function WeatherWidget({ refreshInterval = 600000, }: WeatherWidgetProps) { const [open, setOpen] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); const [selectedCity, setSelectedCity] = useState(city); const [weather, setWeather] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); + + // 표시할 날씨 정보 선택 + const [selectedItems, setSelectedItems] = useState([ + 'temperature', + 'feelsLike', + 'humidity', + 'windSpeed', + 'pressure', + ]); + + // 날씨 항목 정의 + const weatherItems = [ + { id: 'temperature', label: '기온', icon: Sun }, + { id: 'feelsLike', label: '체감온도', icon: Sun }, + { id: 'humidity', label: '습도', icon: Droplets }, + { id: 'windSpeed', label: '풍속', icon: Wind }, + { id: 'pressure', label: '기압', icon: Gauge }, + ]; + + // 항목 토글 + const toggleItem = (itemId: string) => { + setSelectedItems((prev) => + prev.includes(itemId) ? prev.filter((id) => id !== itemId) : [...prev, itemId] + ); + }; // 도시 목록 (전국 시/군/구 단위) const cities = [ @@ -278,9 +305,9 @@ export default function WeatherWidget({ } return ( -
+
{/* 헤더 */} -
+
@@ -334,6 +361,46 @@ export default function WeatherWidget({ : ''}

+ + + + + +
+

표시 항목

+ {weatherItems.map((item) => { + const Icon = item.icon; + return ( + + ); + })} +
+
+
- {/* 날씨 아이콘 및 온도 */} -
-
- {getWeatherIcon(weather.weatherMain)} -
-
- {weather.temperature}°C + {/* 반응형 그리드 레이아웃 - 자동 조정 */} +
+ {/* 날씨 아이콘 및 온도 */} +
+
+
+ {(() => { + const iconClass = "h-5 w-5"; + switch (weather.weatherMain.toLowerCase()) { + case 'clear': + return ; + case 'clouds': + return ; + case 'rain': + case 'drizzle': + return ; + case 'snow': + return ; + default: + return ; + } + })()} +
+
+
+ {weather.temperature}°C +
+

+ {weather.weatherDescription} +

-

- {weather.weatherDescription} -

-
- {/* 상세 정보 */} -
-
- -
-

체감 온도

-

- {weather.feelsLike}°C -

+ {/* 기온 - 선택 가능 */} + {selectedItems.includes('temperature') && ( +
+ +
+

기온

+

+ {weather.temperature}°C +

+
-
-
- -
-

습도

-

- {weather.humidity}% -

+ )} + + {/* 체감 온도 */} + {selectedItems.includes('feelsLike') && ( +
+ +
+

체감온도

+

+ {weather.feelsLike}°C +

+
-
-
- -
-

풍속

-

- {weather.windSpeed} m/s -

+ )} + + {/* 습도 */} + {selectedItems.includes('humidity') && ( +
+ +
+

습도

+

+ {weather.humidity}% +

+
-
-
- -
-

기압

-

- {weather.pressure} hPa -

+ )} + + {/* 풍속 */} + {selectedItems.includes('windSpeed') && ( +
+ +
+

풍속

+

+ {weather.windSpeed} m/s +

+
-
+ )} + + {/* 기압 */} + {selectedItems.includes('pressure') && ( +
+ +
+

기압

+

+ {weather.pressure} hPa +

+
+
+ )}
);