s-s / index.html
joung's picture
Add 3 files
72d5180 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๋‚ ์”จ์˜ˆ๋ณด - ์‹ค์‹œ๊ฐ„ ์ผ๊ธฐ์˜ˆ๋ณด ์„œ๋น„์Šค</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
body {
font-family: 'Noto Sans KR', sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
.weather-card {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
transition: all 0.3s ease;
}
.weather-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px 0 rgba(31, 38, 135, 0.25);
}
.search-input:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
.temperature {
font-size: 4rem;
font-weight: 300;
background: linear-gradient(to right, #3b82f6, #10b981);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hourly-forecast::-webkit-scrollbar {
height: 6px;
}
.hourly-forecast::-webkit-scrollbar-thumb {
background: rgba(59, 130, 246, 0.5);
border-radius: 3px;
}
.loading-spinner {
border-top-color: #3b82f6;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.sunny-bg {
background: linear-gradient(to bottom right, #FFD700, #FF8C00);
}
.cloudy-bg {
background: linear-gradient(to bottom right, #B0C4DE, #778899);
}
.rainy-bg {
background: linear-gradient(to bottom right, #4682B4, #1E90FF);
}
.snowy-bg {
background: linear-gradient(to bottom right, #E6E6FA, #ADD8E6);
}
.thunder-bg {
background: linear-gradient(to bottom right, #4B0082, #9932CC);
}
</style>
</head>
<body class="text-gray-800">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- ํ—ค๋” -->
<header class="flex flex-col md:flex-row justify-between items-center mb-8">
<div class="flex items-center mb-4 md:mb-0">
<i class="fas fa-cloud-sun text-4xl text-blue-500 mr-3"></i>
<h1 class="text-3xl font-bold bg-gradient-to-r from-blue-500 to-green-500 bg-clip-text text-transparent">๋‚ ์”จ์˜ˆ๋ณด</h1>
</div>
<!-- ๊ฒ€์ƒ‰์ฐฝ -->
<div class="relative w-full md:w-64">
<input
type="text"
id="search-input"
placeholder="๋„์‹œ ๊ฒ€์ƒ‰ (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ)"
class="w-full px-4 py-2 pr-10 rounded-full border border-gray-300 focus:border-blue-400 search-input"
>
<button id="search-btn" class="absolute right-3 top-2 text-gray-500 hover:text-blue-500">
<i class="fas fa-search"></i>
</button>
</div>
</header>
<!-- ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ -->
<div id="loading" class="hidden flex justify-center items-center py-20">
<div class="loading-spinner h-12 w-12 border-4 border-blue-200 rounded-full"></div>
</div>
<!-- ๋ฉ”์ธ ๋‚ ์”จ ์ •๋ณด -->
<div id="weather-container" class="hidden">
<!-- ํ˜„์žฌ ๋‚ ์”จ -->
<div id="current-weather" class="weather-card p-6 mb-6">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<div class="flex items-center">
<h2 id="location" class="text-2xl font-bold mr-2">์„œ์šธํŠน๋ณ„์‹œ</h2>
<span id="country" class="text-gray-600">๋Œ€ํ•œ๋ฏผ๊ตญ</span>
</div>
<p id="date-time" class="text-gray-600">2023๋…„ 7์›” 15์ผ ์˜คํ›„ 3:00</p>
<div class="flex items-center mt-2">
<div id="weather-icon" class="text-5xl mr-3">
<i class="fas fa-sun text-yellow-400"></i>
</div>
<p id="weather-description" class="text-lg">๋ง‘์Œ</p>
</div>
</div>
<div class="text-center">
<div class="temperature" id="current-temp">24ยฐC</div>
<div class="flex justify-center space-x-4 mt-2">
<div>
<p class="text-gray-600">์ตœ๊ณ </p>
<p id="max-temp" class="font-medium">28ยฐC</p>
</div>
<div>
<p class="text-gray-600">์ตœ์ €</p>
<p id="min-temp" class="font-medium">18ยฐC</p>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mt-4 md:mt-0">
<div class="flex items-center">
<i class="fas fa-wind text-blue-400 mr-2"></i>
<div>
<p class="text-gray-600">๋ฐ”๋žŒ</p>
<p id="wind-speed" class="font-medium">5 km/h</p>
</div>
</div>
<div class="flex items-center">
<i class="fas fa-tint text-blue-400 mr-2"></i>
<div>
<p class="text-gray-600">์Šต๋„</p>
<p id="humidity" class="font-medium">65%</p>
</div>
</div>
<div class="flex items-center">
<i class="fas fa-compress-arrows-alt text-blue-400 mr-2"></i>
<div>
<p class="text-gray-600">๊ธฐ์••</p>
<p id="pressure" class="font-medium">1012 hPa</p>
</div>
</div>
<div class="flex items-center">
<i class="fas fa-eye text-blue-400 mr-2"></i>
<div>
<p class="text-gray-600">๊ฐ€์‹œ๊ฑฐ๋ฆฌ</p>
<p id="visibility" class="font-medium">10 km</p>
</div>
</div>
</div>
</div>
</div>
<!-- ์‹œ๊ฐ„๋ณ„ ์˜ˆ๋ณด -->
<div class="mb-6">
<h3 class="text-xl font-bold mb-3 flex items-center">
<i class="fas fa-clock text-blue-500 mr-2"></i>
์‹œ๊ฐ„๋ณ„ ์˜ˆ๋ณด
</h3>
<div id="hourly-forecast" class="hourly-forecast flex overflow-x-auto pb-2 space-x-4">
<!-- ์‹œ๊ฐ„๋ณ„ ์˜ˆ๋ณด ์นด๋“œ๋“ค์ด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
</div>
</div>
<!-- ์ฃผ๊ฐ„ ์˜ˆ๋ณด -->
<div>
<h3 class="text-xl font-bold mb-3 flex items-center">
<i class="fas fa-calendar-alt text-blue-500 mr-2"></i>
5์ผ๊ฐ„์˜ ์ผ๊ธฐ์˜ˆ๋ณด
</h3>
<div id="daily-forecast" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<!-- ์ฃผ๊ฐ„ ์˜ˆ๋ณด ์นด๋“œ๋“ค์ด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
</div>
</div>
</div>
<!-- ์ดˆ๊ธฐ ๋ฉ”์‹œ์ง€ -->
<div id="initial-message" class="text-center py-20">
<i class="fas fa-cloud-sun text-6xl text-blue-300 mb-4"></i>
<h2 class="text-2xl font-bold text-gray-700 mb-2">๋‚ ์”จ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜์„ธ์š”</h2>
<p class="text-gray-600 mb-6">๊ฒ€์ƒ‰์ฐฝ์— ๋„์‹œ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜๋ฉด ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
<button id="use-current-location" class="px-6 py-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition">
<i class="fas fa-location-arrow mr-2"></i>ํ˜„์žฌ ์œ„์น˜ ์‚ฌ์šฉ
</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// API ํ‚ค (์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์„œ๋ฒ„ ์‚ฌ์ด๋“œ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค)
const API_KEY = '5d4ae9954b1c58b1a0c1c9f3a0d6a5a';
// DOM ์š”์†Œ
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const useCurrentLocationBtn = document.getElementById('use-current-location');
const weatherContainer = document.getElementById('weather-container');
const loading = document.getElementById('loading');
const initialMessage = document.getElementById('initial-message');
// ๊ธฐ๋ณธ์œผ๋กœ ์„œ์šธ ๋‚ ์”จ ํ‘œ์‹œ
fetchWeather('Seoul');
// ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
searchBtn.addEventListener('click', () => {
const city = searchInput.value.trim();
if (city) {
fetchWeather(city);
}
});
// ์—”ํ„ฐ ํ‚ค ์ด๋ฒคํŠธ
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const city = searchInput.value.trim();
if (city) {
fetchWeather(city);
}
}
});
// ํ˜„์žฌ ์œ„์น˜ ์‚ฌ์šฉ ๋ฒ„ํŠผ
useCurrentLocationBtn.addEventListener('click', () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
fetchWeatherByCoords(latitude, longitude);
},
(error) => {
alert('์œ„์น˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋™์œผ๋กœ ๋„์‹œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
console.error(error);
}
);
} else {
alert('์ด ๋ธŒ๋ผ์šฐ์ €๋Š” ์œ„์น˜ ์ •๋ณด ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
}
});
// ๋‚ ์”จ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
function fetchWeather(city) {
loading.classList.remove('hidden');
weatherContainer.classList.add('hidden');
initialMessage.classList.add('hidden');
fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric&lang=kr`)
.then(response => {
if (!response.ok) {
throw new Error('๋„์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค');
}
return response.json();
})
.then(data => {
displayCurrentWeather(data);
return fetch(`https://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=${API_KEY}&units=metric&lang=kr`);
})
.then(response => response.json())
.then(data => {
displayForecast(data);
loading.classList.add('hidden');
weatherContainer.classList.remove('hidden');
})
.catch(error => {
console.error('Error:', error);
alert(error.message);
loading.classList.add('hidden');
initialMessage.classList.remove('hidden');
});
}
// ์ขŒํ‘œ๋กœ ๋‚ ์”จ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
function fetchWeatherByCoords(lat, lon) {
loading.classList.remove('hidden');
weatherContainer.classList.add('hidden');
initialMessage.classList.add('hidden');
fetch(`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric&lang=kr`)
.then(response => response.json())
.then(data => {
displayCurrentWeather(data);
return fetch(`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric&lang=kr`);
})
.then(response => response.json())
.then(data => {
displayForecast(data);
loading.classList.add('hidden');
weatherContainer.classList.remove('hidden');
})
.catch(error => {
console.error('Error:', error);
alert('๋‚ ์”จ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค');
loading.classList.add('hidden');
initialMessage.classList.remove('hidden');
});
}
// ํ˜„์žฌ ๋‚ ์”จ ํ‘œ์‹œ
function displayCurrentWeather(data) {
const location = document.getElementById('location');
const country = document.getElementById('country');
const dateTime = document.getElementById('date-time');
const weatherIcon = document.getElementById('weather-icon');
const weatherDescription = document.getElementById('weather-description');
const currentTemp = document.getElementById('current-temp');
const maxTemp = document.getElementById('max-temp');
const minTemp = document.getElementById('min-temp');
const windSpeed = document.getElementById('wind-speed');
const humidity = document.getElementById('humidity');
const pressure = document.getElementById('pressure');
const visibility = document.getElementById('visibility');
// ์œ„์น˜ ์ •๋ณด
location.textContent = data.name;
country.textContent = data.sys.country;
// ๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„
const now = new Date();
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
};
dateTime.textContent = now.toLocaleDateString('ko-KR', options);
// ๋‚ ์”จ ์ •๋ณด
const weather = data.weather[0];
weatherDescription.textContent = weather.description;
currentTemp.textContent = `${Math.round(data.main.temp)}ยฐC`;
maxTemp.textContent = `${Math.round(data.main.temp_max)}ยฐC`;
minTemp.textContent = `${Math.round(data.main.temp_min)}ยฐC`;
windSpeed.textContent = `${Math.round(data.wind.speed * 3.6)} km/h`;
humidity.textContent = `${data.main.humidity}%`;
pressure.textContent = `${data.main.pressure} hPa`;
visibility.textContent = `${data.visibility / 1000} km`;
// ๋‚ ์”จ ์•„์ด์ฝ˜ ์„ค์ •
setWeatherIcon(weatherIcon, weather.id, data.dt);
// ๋ฐฐ๊ฒฝ ์ƒ‰์ƒ ๋ณ€๊ฒฝ
setBackgroundByWeather(weather.id);
}
// ์˜ˆ๋ณด ์ •๋ณด ํ‘œ์‹œ
function displayForecast(data) {
const hourlyForecast = document.getElementById('hourly-forecast');
const dailyForecast = document.getElementById('daily-forecast');
// ์‹œ๊ฐ„๋ณ„ ์˜ˆ๋ณด (๋‹ค์Œ 12์‹œ๊ฐ„)
hourlyForecast.innerHTML = '';
for (let i = 0; i < 12; i++) {
const forecast = data.list[i];
const date = new Date(forecast.dt * 1000);
const hour = date.getHours();
const card = document.createElement('div');
card.className = 'weather-card flex-shrink-0 p-4 text-center min-w-[80px]';
card.innerHTML = `
<p class="font-medium">${hour}์‹œ</p>
<div class="my-2 text-2xl">
${getWeatherIcon(forecast.weather[0].id, forecast.dt)}
</div>
<p class="font-bold">${Math.round(forecast.main.temp)}ยฐC</p>
<p class="text-sm text-gray-600">${forecast.weather[0].description}</p>
`;
hourlyForecast.appendChild(card);
}
// ์ผ๋ณ„ ์˜ˆ๋ณด (5์ผ)
dailyForecast.innerHTML = '';
const dailyData = {};
// ๊ฐ™์€ ๋‚ ์งœ์˜ ๋ฐ์ดํ„ฐ ๊ทธ๋ฃนํ™”
data.list.forEach(forecast => {
const date = new Date(forecast.dt * 1000);
const dateStr = date.toLocaleDateString('ko-KR', { weekday: 'long', month: 'short', day: 'numeric' });
if (!dailyData[dateStr]) {
dailyData[dateStr] = {
temps: [],
weather: [],
dt: forecast.dt
};
}
dailyData[dateStr].temps.push(forecast.main.temp);
dailyData[dateStr].weather.push(forecast.weather[0]);
});
// ๊ฐ ๋‚ ์งœ๋ณ„๋กœ ์นด๋“œ ์ƒ์„ฑ
let count = 0;
for (const [date, info] of Object.entries(dailyData)) {
if (count >= 5) break;
// ๊ฐ€์žฅ ๋นˆ๋ฒˆํ•œ ๋‚ ์”จ ์ƒํƒœ ์ฐพ๊ธฐ
const weatherCount = {};
info.weather.forEach(w => {
const key = w.id;
weatherCount[key] = (weatherCount[key] || 0) + 1;
});
const mostFrequentWeatherId = parseInt(Object.keys(weatherCount).reduce((a, b) =>
weatherCount[a] > weatherCount[b] ? a : b
));
const mostFrequentWeather = info.weather.find(w => w.id === mostFrequentWeatherId);
// ์ตœ๊ณ /์ตœ์ € ๊ธฐ์˜จ ๊ณ„์‚ฐ
const maxTemp = Math.round(Math.max(...info.temps));
const minTemp = Math.round(Math.min(...info.temps));
const card = document.createElement('div');
card.className = 'weather-card p-4';
card.innerHTML = `
<p class="font-bold text-lg mb-2">${date.split(' ')[0]} ${date.split(' ')[1]} ${date.split(' ')[2]}</p>
<div class="flex items-center justify-between">
<div class="text-4xl">
${getWeatherIcon(mostFrequentWeather.id, info.dt)}
</div>
<div class="text-right">
<p class="font-medium">${mostFrequentWeather.description}</p>
<p class="text-2xl font-bold">${Math.round((maxTemp + minTemp) / 2)}ยฐC</p>
<p class="text-sm text-gray-600">${maxTemp}ยฐ / ${minTemp}ยฐ</p>
</div>
</div>
`;
dailyForecast.appendChild(card);
count++;
}
}
// ๋‚ ์”จ ์•„์ด์ฝ˜ ์„ค์ •
function setWeatherIcon(element, weatherId, timestamp) {
const isDay = isDayTime(timestamp);
element.innerHTML = getWeatherIcon(weatherId, timestamp, isDay);
}
// ๋‚ ์”จ ID์— ๋”ฐ๋ฅธ ์•„์ด์ฝ˜ ๋ฐ˜ํ™˜
function getWeatherIcon(weatherId, timestamp, isDay = null) {
if (isDay === null) {
isDay = isDayTime(timestamp);
}
// ๋‚ ์”จ ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ์•„์ด์ฝ˜ ์„ค์ • (OpenWeatherMap ๊ธฐ์ค€)
if (weatherId >= 200 && weatherId < 300) {
return '<i class="fas fa-bolt text-purple-400"></i>'; // ๋ฒˆ๊ฐœ
} else if (weatherId >= 300 && weatherId < 400) {
return '<i class="fas fa-cloud-rain text-blue-400"></i>'; // ์ด์Šฌ๋น„
} else if (weatherId >= 500 && weatherId < 600) {
return '<i class="fas fa-umbrella text-blue-500"></i>'; // ๋น„
} else if (weatherId >= 600 && weatherId < 700) {
return '<i class="far fa-snowflake text-blue-200"></i>'; // ๋ˆˆ
} else if (weatherId >= 700 && weatherId < 800) {
return '<i class="fas fa-smog text-gray-400"></i>'; // ์•ˆ๊ฐœ ๋“ฑ
} else if (weatherId === 800) {
return isDay
? '<i class="fas fa-sun text-yellow-400"></i>'
: '<i class="fas fa-moon text-blue-200"></i>';
} else if (weatherId === 801) {
return isDay
? '<i class="fas fa-cloud-sun text-yellow-300"></i>'
: '<i class="fas fa-cloud-moon text-blue-200"></i>';
} else if (weatherId > 801 && weatherId < 805) {
return '<i class="fas fa-cloud text-gray-300"></i>';
} else {
return '<i class="fas fa-question-circle text-gray-400"></i>';
}
}
// ๋ฐฐ๊ฒฝ ์ƒ‰์ƒ ์„ค์ •
function setBackgroundByWeather(weatherId) {
const body = document.body;
// ๊ธฐ์กด ํด๋ž˜์Šค ์ œ๊ฑฐ
body.classList.remove(
'sunny-bg', 'cloudy-bg', 'rainy-bg',
'snowy-bg', 'thunder-bg'
);
// ๋‚ ์”จ์— ๋”ฐ๋ฅธ ๋ฐฐ๊ฒฝ ํด๋ž˜์Šค ์ถ”๊ฐ€
if (weatherId >= 200 && weatherId < 300) {
body.classList.add('thunder-bg');
} else if ((weatherId >= 300 && weatherId < 600) || weatherId === 701) {
body.classList.add('rainy-bg');
} else if (weatherId >= 600 && weatherId < 700) {
body.classList.add('snowy-bg');
} else if (weatherId === 800) {
body.classList.add('sunny-bg');
} else {
body.classList.add('cloudy-bg');
}
}
// ๋‚ฎ/๋ฐค ํ™•์ธ
function isDayTime(timestamp) {
const date = new Date(timestamp * 1000);
const hours = date.getHours();
return hours >= 6 && hours < 18;
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - ๐Ÿงฌ <a href="https://enzostvs-deepsite.hf.space?remix=joung/s-s" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>