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();