<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fancy Weather Widget - Real-time Edition</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=Noto+Sans+KR:wght@300;400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', 'Noto Sans KR', sans-serif;
transition: background 1s ease;
}
.bg-animate {
background-size: 400% 400%;
animation: gradientMove 15s ease infinite;
}
@keyframes gradientMove {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.glass-panel {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.6);
}
.float-icon {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
.rain { position: absolute; width: 100%; height: 100%; z-index: 0; overflow: hidden; pointer-events: none; display: none; }
.drop { position: absolute; background: rgba(255, 255, 255, 0.2); width: 1px; height: 20px; top: -20px; animation: fall linear infinite; }
@keyframes fall { to { transform: translateY(100vh); } }
.stars { position: absolute; width: 100%; height: 100%; z-index: 0; pointer-events: none; display: none; }
.star { position: absolute; background: white; border-radius: 50%; animation: twinkle ease-in-out infinite; }
@keyframes twinkle { 0%, 100% { opacity: 0.3; transform: scale(1); } 50% { opacity: 1; transform: scale(1.2); } }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* 로딩 애니메이션 */
.loading-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
</style>
</head>
<body id="body-bg" class="h-screen w-full flex items-center justify-center bg-animate bg-gradient-to-br from-slate-900 via-gray-900 to-black text-white overflow-hidden relative">
<div id="rain-layer" class="rain"></div>
<div id="star-layer" class="stars"></div>
<div class="relative z-10 w-full max-w-md p-6 mx-4 rounded-3xl glass-panel transform transition-all duration-500 hover:scale-[1.01]">
<div class="flex justify-between items-start mb-8">
<div onclick="fetchWeather()" class="cursor-pointer group">
<h2 class="text-2xl font-bold flex items-center gap-2 text-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-400 group-hover:animate-bounce"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
충남 홍성군
</h2>
<p id="current-date" class="text-sm text-slate-400 mt-1 font-medium">로딩 중...</p>
</div>
<div id="live-indicator" class="px-3 py-1 rounded-full bg-slate-800/60 text-xs font-bold text-slate-500 backdrop-blur-md border border-white/5 tracking-wider transition-all">
SYNCING
</div>
</div>
<div class="flex flex-col items-center justify-center mb-8 relative">
<div id="weather-icon" class="mb-4 float-icon drop-shadow-[0_0_25px_rgba(56,189,248,0.3)]">
<!-- 날씨 아이콘이 동적으로 삽입됨 -->
<div class="w-24 h-24 bg-slate-800 rounded-full animate-pulse"></div>
</div>
<h1 id="main-temp" class="text-7xl font-light tracking-tighter text-white drop-shadow-2xl">--°</h1>
<p id="weather-text" class="text-xl font-semibold mt-2 text-blue-100">데이터를 가져오는 중...</p>
<p id="temp-range" class="text-sm text-slate-400 mt-1 font-medium italic">최고 --° / 최저 --°</p>
</div>
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="glass-panel rounded-2xl p-3 flex flex-col items-center justify-center bg-slate-800/40 border-0">
<svg class="w-6 h-6 mb-1 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.414 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">습도</span>
<span id="humidity" class="font-bold text-slate-200">--%</span>
</div>
<div class="glass-panel rounded-2xl p-3 flex flex-col items-center justify-center bg-slate-800/40 border-0">
<svg class="w-6 h-6 mb-1 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">체감</span>
<span id="feels-like" class="font-bold text-emerald-400">--°</span>
</div>
<div class="glass-panel rounded-2xl p-3 flex flex-col items-center justify-center bg-slate-800/40 border-0">
<svg class="w-6 h-6 mb-1 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">풍속</span>
<span id="wind-speed" class="font-bold text-slate-200">--m/s</span>
</div>
</div>
<div class="mb-2">
<h3 class="text-xs font-bold mb-3 text-slate-500 uppercase tracking-widest px-1">기상 스테이션 안내</h3>
<p class="text-[10px] text-slate-500 px-1 leading-relaxed italic">충남 홍성군 좌표(36.60, 126.66) 기준 실시간 데이터입니다. 지명을 클릭하면 즉시 갱신됩니다.</p>
</div>
</div>
<script>
// 홍성군 좌표 설정
const LAT = 36.601;
const LON = 126.660;
const backgrounds = {
sunny: "from-slate-900 via-zinc-900 to-black",
cloudy: "from-gray-950 via-slate-900 to-zinc-900",
rainy: "from-slate-950 via-indigo-950 to-black",
snowy: "from-slate-100 via-blue-100 to-white",
night: "from-black via-slate-950 to-slate-900"
};
const icons = {
sunny: `<svg width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-400 fill-yellow-400/10"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`,
cloudy: `<svg width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-slate-400 fill-slate-400/10"><path d="M17.5 19a5.5 5.5 0 0 0 5.5-5.5v-1a5.5 5.5 0 0 0-5.5-5.5H16a7.5 7.5 0 1 0-14 4.5V13a6 6 0 0 0 6 6Z"/></svg>`,
rainy: `<svg width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-blue-400 fill-blue-400/10"><path d="M17.5 19a5.5 5.5 0 0 0 5.5-5.5v-1a5.5 5.5 0 0 0-5.5-5.5H16a7.5 7.5 0 1 0-14 4.5V13a6 6 0 0 0 6 6Z"/><path d="M8 20v2"/><path d="M12 20v2"/><path d="M16 20v2"/></svg>`,
snowy: `<svg width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-blue-200"><path d="M12 2v20"/><path d="m20 12-8 8-8-8"/><path d="m20 4-8 8-8-8"/></svg>`,
night: `<svg width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-indigo-300 fill-indigo-300/10"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>`
};
function updateDateTime() {
const dateEl = document.getElementById('current-date');
const now = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
dateEl.textContent = now.toLocaleDateString('ko-KR', options);
}
async function fetchWeather() {
const indicator = document.getElementById('live-indicator');
indicator.textContent = "SYNCING";
indicator.className = "px-3 py-1 rounded-full bg-slate-800/60 text-xs font-bold text-blue-400 backdrop-blur-md border border-blue-500/30 tracking-wider transition-all loading-pulse";
try {
// Open-Meteo 무료 API 호출
const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min&timezone=Asia%2FSeoul`);
const data = await response.json();
renderWeather(data);
indicator.textContent = "LIVE";
indicator.className = "px-3 py-1 rounded-full bg-emerald-900/40 text-xs font-bold text-emerald-400 backdrop-blur-md border border-emerald-500/30 tracking-wider transition-all";
} catch (error) {
console.error("날씨 데이터 로드 실패:", error);
document.getElementById('weather-text').textContent = "연결 오류";
indicator.textContent = "OFFLINE";
indicator.className = "px-3 py-1 rounded-full bg-red-900/40 text-xs font-bold text-red-400 backdrop-blur-md border border-red-500/30 tracking-wider transition-all";
}
}
function renderWeather(data) {
const current = data.current;
const daily = data.daily;
const code = current.weather_code;
const hour = new Date().getHours();
const isNight = hour >= 18 || hour <= 6;
// UI 업데이트
document.getElementById('main-temp').textContent = `${Math.round(current.temperature_2m)}°`;
document.getElementById('humidity').textContent = `${current.relative_humidity_2m}%`;
document.getElementById('feels-like').textContent = `${Math.round(current.apparent_temperature)}°`;
document.getElementById('wind-speed').textContent = `${current.wind_speed_10m}m/s`;
document.getElementById('temp-range').textContent = `최고 ${Math.round(daily.temperature_2m_max[0])}° / 최저 ${Math.round(daily.temperature_2m_min[0])}°`;
// 상태 매핑 및 테마 결정
let status = "맑음";
let theme = isNight ? 'night' : 'sunny';
if (code >= 1 && code <= 3) {
status = "구름 조금";
theme = isNight ? 'night' : 'cloudy';
} else if (code >= 45 && code <= 48) {
status = "안개";
theme = 'cloudy';
} else if (code >= 51 && code <= 67) {
status = "비";
theme = 'rainy';
} else if (code >= 71 && code <= 77) {
status = "눈";
theme = 'snowy';
} else if (code >= 80 && code <= 99) {
status = "소나기/뇌우";
theme = 'rainy';
}
document.getElementById('weather-text').textContent = status;
document.getElementById('weather-icon').innerHTML = icons[theme];
// 다크/라이트 모드 텍스트 컬러 보정 (눈 테마일 때)
const bodyBg = document.getElementById('body-bg');
bodyBg.className = `h-screen w-full flex items-center justify-center bg-animate bg-gradient-to-br overflow-hidden relative ${backgrounds[theme]}`;
if (theme === 'snowy') {
bodyBg.classList.add('text-slate-800');
bodyBg.classList.remove('text-white');
} else {
bodyBg.classList.remove('text-slate-800');
bodyBg.classList.add('text-white');
}
// 배경 레이어 제어
document.getElementById('rain-layer').style.display = theme === 'rainy' ? 'block' : 'none';
document.getElementById('star-layer').style.display = theme === 'night' ? 'block' : 'none';
if (theme === 'rainy') createRain();
if (theme === 'night') createStars();
}
function createRain() {
const layer = document.getElementById('rain-layer');
layer.innerHTML = '';
for(let i=0; i<40; i++) {
let drop = document.createElement('div');
drop.classList.add('drop');
drop.style.left = Math.random() * 100 + '%';
drop.style.animationDuration = Math.random() * 0.5 + 0.5 + 's';
drop.style.animationDelay = Math.random() * 2 + 's';
layer.appendChild(drop);
}
}
function createStars() {
const layer = document.getElementById('star-layer');
layer.innerHTML = '';
for(let i=0; i<60; i++) {
let star = document.createElement('div');
star.classList.add('star');
let size = Math.random() * 2 + 'px';
star.style.width = size;
star.style.height = size;
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.animationDuration = Math.random() * 3 + 2 + 's';
layer.appendChild(star);
}
}
// 앱 초기화
updateDateTime();
fetchWeather();
// 10분마다 자동 갱신
setInterval(() => {
updateDateTime();
fetchWeather();
}, 600000);
</script>
</body>
</html>