환율 위젯과 날씨 위젯
This commit is contained in:
@@ -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<WeatherData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
|
||||
// 표시할 날씨 정보 선택
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([
|
||||
'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 (
|
||||
<div className="h-full bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border p-6">
|
||||
<div className="h-full bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -334,6 +361,46 @@ export default function WeatherWidget({
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-3" align="end">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">표시 항목</h4>
|
||||
{weatherItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => toggleItem(item.id)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs transition-colors',
|
||||
selectedItems.includes(item.id)
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'h-3.5 w-3.5',
|
||||
selectedItems.includes(item.id) ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -345,59 +412,104 @@ export default function WeatherWidget({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 날씨 아이콘 및 온도 */}
|
||||
<div className="flex items-center justify-center mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
{getWeatherIcon(weather.weatherMain)}
|
||||
<div>
|
||||
<div className="text-5xl font-bold text-gray-900">
|
||||
{weather.temperature}°C
|
||||
{/* 반응형 그리드 레이아웃 - 자동 조정 */}
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
|
||||
{/* 날씨 아이콘 및 온도 */}
|
||||
<div className="bg-white/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex-shrink-0">
|
||||
{(() => {
|
||||
const iconClass = "h-5 w-5";
|
||||
switch (weather.weatherMain.toLowerCase()) {
|
||||
case 'clear':
|
||||
return <Sun className={`${iconClass} text-yellow-500`} />;
|
||||
case 'clouds':
|
||||
return <Cloud className={`${iconClass} text-gray-400`} />;
|
||||
case 'rain':
|
||||
case 'drizzle':
|
||||
return <CloudRain className={`${iconClass} text-blue-500`} />;
|
||||
case 'snow':
|
||||
return <CloudSnow className={`${iconClass} text-blue-300`} />;
|
||||
default:
|
||||
return <Cloud className={`${iconClass} text-gray-400`} />;
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-bold text-gray-900 leading-tight truncate">
|
||||
{weather.temperature}°C
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 capitalize leading-tight truncate">
|
||||
{weather.weatherDescription}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 capitalize">
|
||||
{weather.weatherDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상세 정보 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
|
||||
<Wind className="h-5 w-5 text-blue-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">체감 온도</p>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{weather.feelsLike}°C
|
||||
</p>
|
||||
{/* 기온 - 선택 가능 */}
|
||||
{selectedItems.includes('temperature') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Sun className="h-3.5 w-3.5 text-orange-500 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">기온</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
{weather.temperature}°C
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
|
||||
<Droplets className="h-5 w-5 text-blue-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">습도</p>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{weather.humidity}%
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 체감 온도 */}
|
||||
{selectedItems.includes('feelsLike') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Wind className="h-3.5 w-3.5 text-blue-500 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">체감온도</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
{weather.feelsLike}°C
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
|
||||
<Wind className="h-5 w-5 text-blue-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">풍속</p>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{weather.windSpeed} m/s
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 습도 */}
|
||||
{selectedItems.includes('humidity') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Droplets className="h-3.5 w-3.5 text-blue-500 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">습도</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
{weather.humidity}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
|
||||
<Gauge className="h-5 w-5 text-blue-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">기압</p>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{weather.pressure} hPa
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 풍속 */}
|
||||
{selectedItems.includes('windSpeed') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Wind className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">풍속</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
{weather.windSpeed} m/s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 기압 */}
|
||||
{selectedItems.includes('pressure') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Gauge className="h-3.5 w-3.5 text-purple-500 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">기압</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
{weather.pressure} hPa
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user