app.js

Download
javascript 334 lines 10.2 KB
  1/*
  2 * Weather App
  3 * OpenWeatherMap API๋ฅผ ์‚ฌ์šฉํ•œ ๋‚ ์”จ ์•ฑ
  4 *
  5 * ์ฃผ์˜: ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” API ํ‚ค๋ฅผ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ ์„œ๋ฒ„์—์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  6 * ์ด ์˜ˆ์ œ๋Š” ํ•™์Šต์šฉ์œผ๋กœ ๋ฌด๋ฃŒ API๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  7 */
  8
  9// ============================================
 10// ์„ค์ •
 11// ============================================
 12// ๋ฌด๋ฃŒ ๋ฐ๋ชจ API (์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ์ž์ฒด ํ‚ค ํ•„์š”)
 13// https://openweathermap.org/api ์—์„œ ๋ฌด๋ฃŒ ํ‚ค ๋ฐœ๊ธ‰ ๊ฐ€๋Šฅ
 14const API_KEY = 'demo'; // ์‹ค์ œ ํ‚ค๋กœ ๊ต์ฒด ํ•„์š”
 15const API_BASE = 'https://api.openweathermap.org/data/2.5/weather';
 16
 17// ๋ฐ๋ชจ ๋ชจ๋“œ (API ํ‚ค๊ฐ€ ์—†์„ ๋•Œ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ)
 18const DEMO_MODE = API_KEY === 'demo';
 19
 20// ============================================
 21// DOM Elements
 22// ============================================
 23const cityInput = document.getElementById('cityInput');
 24const searchBtn = document.getElementById('searchBtn');
 25const quickCityBtns = document.querySelectorAll('.city-btn');
 26const loadingEl = document.getElementById('loading');
 27const errorEl = document.getElementById('error');
 28const errorMessageEl = document.getElementById('errorMessage');
 29const weatherDisplay = document.getElementById('weatherDisplay');
 30
 31// ============================================
 32// ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ (๋ฐ๋ชจ์šฉ)
 33// ============================================
 34const sampleWeatherData = {
 35    'Seoul': {
 36        name: 'Seoul',
 37        sys: { country: 'KR', sunrise: 1706400000, sunset: 1706436000 },
 38        main: { temp: 3, feels_like: -1, humidity: 45, pressure: 1020 },
 39        weather: [{ main: 'Clear', description: '๋ง‘์Œ', icon: '01d' }],
 40        wind: { speed: 2.5 },
 41        visibility: 10000,
 42        clouds: { all: 10 }
 43    },
 44    'Tokyo': {
 45        name: 'Tokyo',
 46        sys: { country: 'JP', sunrise: 1706396400, sunset: 1706432400 },
 47        main: { temp: 8, feels_like: 5, humidity: 55, pressure: 1015 },
 48        weather: [{ main: 'Clouds', description: '๊ตฌ๋ฆ„ ์กฐ๊ธˆ', icon: '02d' }],
 49        wind: { speed: 3.1 },
 50        visibility: 8000,
 51        clouds: { all: 25 }
 52    },
 53    'New York': {
 54        name: 'New York',
 55        sys: { country: 'US', sunrise: 1706443200, sunset: 1706479200 },
 56        main: { temp: -2, feels_like: -7, humidity: 60, pressure: 1008 },
 57        weather: [{ main: 'Snow', description: '๋ˆˆ', icon: '13d' }],
 58        wind: { speed: 5.2 },
 59        visibility: 3000,
 60        clouds: { all: 90 }
 61    },
 62    'London': {
 63        name: 'London',
 64        sys: { country: 'GB', sunrise: 1706428800, sunset: 1706461200 },
 65        main: { temp: 6, feels_like: 3, humidity: 80, pressure: 1012 },
 66        weather: [{ main: 'Rain', description: '๋น„', icon: '10d' }],
 67        wind: { speed: 4.1 },
 68        visibility: 6000,
 69        clouds: { all: 75 }
 70    },
 71    'Paris': {
 72        name: 'Paris',
 73        sys: { country: 'FR', sunrise: 1706425200, sunset: 1706458800 },
 74        main: { temp: 5, feels_like: 2, humidity: 70, pressure: 1010 },
 75        weather: [{ main: 'Clouds', description: 'ํ๋ฆผ', icon: '04d' }],
 76        wind: { speed: 3.5 },
 77        visibility: 7000,
 78        clouds: { all: 65 }
 79    }
 80};
 81
 82// ============================================
 83// ์ดˆ๊ธฐํ™”
 84// ============================================
 85function init() {
 86    addEventListeners();
 87
 88    // ๋ฐ๋ชจ ๋ชจ๋“œ ์•Œ๋ฆผ
 89    if (DEMO_MODE) {
 90        console.log('๋ฐ๋ชจ ๋ชจ๋“œ: ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.');
 91        console.log('์‹ค์ œ API๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด app.js์˜ API_KEY๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.');
 92    }
 93
 94    // ์ดˆ๊ธฐ ๋„์‹œ ๋กœ๋“œ
 95    fetchWeather('Seoul');
 96}
 97
 98// ============================================
 99// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
100// ============================================
101function addEventListeners() {
102    // ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ
103    searchBtn.addEventListener('click', () => {
104        const city = cityInput.value.trim();
105        if (city) {
106            fetchWeather(city);
107        }
108    });
109
110    // Enter ํ‚ค
111    cityInput.addEventListener('keypress', (e) => {
112        if (e.key === 'Enter') {
113            const city = cityInput.value.trim();
114            if (city) {
115                fetchWeather(city);
116            }
117        }
118    });
119
120    // ๋น ๋ฅธ ๋„์‹œ ๋ฒ„ํŠผ
121    quickCityBtns.forEach(btn => {
122        btn.addEventListener('click', () => {
123            const city = btn.dataset.city;
124            cityInput.value = city;
125            fetchWeather(city);
126        });
127    });
128}
129
130// ============================================
131// ๋‚ ์”จ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
132// ============================================
133async function fetchWeather(city) {
134    showLoading();
135    hideError();
136    hideWeather();
137
138    try {
139        let data;
140
141        if (DEMO_MODE) {
142            // ๋ฐ๋ชจ ๋ชจ๋“œ: ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
143            await simulateDelay(800);
144            data = getDemoData(city);
145        } else {
146            // ์‹ค์ œ API ํ˜ธ์ถœ
147            const url = `${API_BASE}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=metric&lang=kr`;
148            const response = await fetch(url);
149
150            if (!response.ok) {
151                if (response.status === 404) {
152                    throw new Error(`'${city}'๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`);
153                } else if (response.status === 401) {
154                    throw new Error('API ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
155                } else {
156                    throw new Error('๋‚ ์”จ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
157                }
158            }
159
160            data = await response.json();
161        }
162
163        displayWeather(data);
164    } catch (error) {
165        showError(error.message);
166    } finally {
167        hideLoading();
168    }
169}
170
171// ============================================
172// ๋ฐ๋ชจ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
173// ============================================
174function getDemoData(city) {
175    // ์ •ํ™•ํ•œ ์ด๋ฆ„ ๋งค์นญ
176    if (sampleWeatherData[city]) {
177        return sampleWeatherData[city];
178    }
179
180    // ๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ ๋งค์นญ
181    const cityLower = city.toLowerCase();
182    for (const [key, value] of Object.entries(sampleWeatherData)) {
183        if (key.toLowerCase() === cityLower) {
184            return value;
185        }
186    }
187
188    // ํ•œ๊ธ€ ๋„์‹œ๋ช… ๋งคํ•‘
189    const koreanCities = {
190        '์„œ์šธ': 'Seoul',
191        '๋„์ฟ„': 'Tokyo',
192        '๋‰ด์š•': 'New York',
193        '๋Ÿฐ๋˜': 'London',
194        'ํŒŒ๋ฆฌ': 'Paris'
195    };
196
197    if (koreanCities[city]) {
198        return sampleWeatherData[koreanCities[city]];
199    }
200
201    // ์ฐพ์„ ์ˆ˜ ์—†์Œ
202    throw new Error(`'${city}'๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ฐ๋ชจ ๋ชจ๋“œ์—์„œ๋Š” Seoul, Tokyo, New York, London, Paris๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.`);
203}
204
205function simulateDelay(ms) {
206    return new Promise(resolve => setTimeout(resolve, ms));
207}
208
209// ============================================
210// ๋‚ ์”จ ํ‘œ์‹œ
211// ============================================
212function displayWeather(data) {
213    // ๋„์‹œ ์ •๋ณด
214    document.getElementById('cityName').textContent = data.name;
215    document.getElementById('country').textContent = getCountryName(data.sys.country);
216
217    // ์˜จ๋„
218    document.getElementById('temp').textContent = Math.round(data.main.temp);
219
220    // ๋‚ ์”จ ์•„์ด์ฝ˜
221    const iconCode = data.weather[0].icon;
222    const iconUrl = `https://openweathermap.org/img/wn/${iconCode}@2x.png`;
223    document.getElementById('weatherIcon').src = iconUrl;
224    document.getElementById('weatherIcon').alt = data.weather[0].description;
225
226    // ์„ค๋ช…
227    document.getElementById('description').textContent = data.weather[0].description;
228
229    // ์ƒ์„ธ ์ •๋ณด
230    document.getElementById('feelsLike').textContent = `${Math.round(data.main.feels_like)}ยฐC`;
231    document.getElementById('humidity').textContent = `${data.main.humidity}%`;
232    document.getElementById('windSpeed').textContent = `${data.wind.speed} m/s`;
233    document.getElementById('pressure').textContent = `${data.main.pressure} hPa`;
234    document.getElementById('visibility').textContent = `${(data.visibility / 1000).toFixed(1)} km`;
235    document.getElementById('clouds').textContent = `${data.clouds.all}%`;
236
237    // ์ผ์ถœ/์ผ๋ชฐ
238    document.getElementById('sunrise').textContent = formatTime(data.sys.sunrise);
239    document.getElementById('sunset').textContent = formatTime(data.sys.sunset);
240
241    // ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„
242    document.getElementById('updateTime').textContent = new Date().toLocaleTimeString('ko-KR');
243
244    // ๋ฐฐ๊ฒฝ ๋ณ€๊ฒฝ (๋‚ ์”จ์— ๋”ฐ๋ผ)
245    updateBackground(data.weather[0].main);
246
247    showWeather();
248}
249
250// ============================================
251// ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
252// ============================================
253function formatTime(timestamp) {
254    const date = new Date(timestamp * 1000);
255    return date.toLocaleTimeString('ko-KR', {
256        hour: '2-digit',
257        minute: '2-digit'
258    });
259}
260
261function getCountryName(code) {
262    const countries = {
263        'KR': '๋Œ€ํ•œ๋ฏผ๊ตญ',
264        'JP': '์ผ๋ณธ',
265        'US': '๋ฏธ๊ตญ',
266        'GB': '์˜๊ตญ',
267        'FR': 'ํ”„๋ž‘์Šค',
268        'CN': '์ค‘๊ตญ',
269        'DE': '๋…์ผ',
270        'IT': '์ดํƒˆ๋ฆฌ์•„',
271        'ES': '์ŠคํŽ˜์ธ',
272        'AU': 'ํ˜ธ์ฃผ'
273    };
274    return countries[code] || code;
275}
276
277function updateBackground(weatherMain) {
278    const app = document.querySelector('.app');
279
280    // ๊ธฐ์กด ํด๋ž˜์Šค ์ œ๊ฑฐ
281    app.classList.remove('sunny', 'cloudy', 'rainy', 'snowy');
282
283    // ๋‚ ์”จ์— ๋”ฐ๋ฅธ ํด๋ž˜์Šค ์ถ”๊ฐ€
284    switch (weatherMain.toLowerCase()) {
285        case 'clear':
286            app.classList.add('sunny');
287            break;
288        case 'clouds':
289            app.classList.add('cloudy');
290            break;
291        case 'rain':
292        case 'drizzle':
293        case 'thunderstorm':
294            app.classList.add('rainy');
295            break;
296        case 'snow':
297            app.classList.add('snowy');
298            break;
299    }
300}
301
302// ============================================
303// UI ์ƒํƒœ ๊ด€๋ฆฌ
304// ============================================
305function showLoading() {
306    loadingEl.classList.remove('hidden');
307}
308
309function hideLoading() {
310    loadingEl.classList.add('hidden');
311}
312
313function showError(message) {
314    errorMessageEl.textContent = message;
315    errorEl.classList.remove('hidden');
316}
317
318function hideError() {
319    errorEl.classList.add('hidden');
320}
321
322function showWeather() {
323    weatherDisplay.classList.remove('hidden');
324}
325
326function hideWeather() {
327    weatherDisplay.classList.add('hidden');
328}
329
330// ============================================
331// ์•ฑ ์‹œ์ž‘
332// ============================================
333init();