From e2f00c6e21e7ae2c1393bc4654db68aa48520bc1 Mon Sep 17 00:00:00 2001 From: "kihong.kim" Date: Sat, 31 Jan 2026 21:19:13 +0900 Subject: [PATCH] feat: Enhance TV support and Daily Weather Forecast (w/ Air Quality) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Frontend (Flutter)** - Optimize for Google TV: Force Landscape mode, disable touchscreen requirement, and set TV Dashboard as default home. - Weather Widget: - Add Daily Max/Min temperature ('최고/최저'). - Add Air Quality Index (AQI) with visual indicator ('미세먼지'). - Increase font sizes and adjust layout to match Digital Clock style. - Implement 1-hour auto-refresh timer. - Model: Update WeatherInfo to support tempMin, tempMax, and aqi. - **Backend (Node.js)** - Weather API (/api/weather): - Implement daily forecast aggregation logic (Seoul Time KST) to calculate accurate daily High/Low. - Integrate OpenWeatherMap Air Pollution API to fetch AQI. --- backend/routes/weather.js | 51 +++++- .../android/app/src/main/AndroidManifest.xml | 4 + flutter_app/lib/main.dart | 18 +- flutter_app/lib/models/weather_info.dart | 9 + flutter_app/lib/services/mock_data.dart | 3 + flutter_app/lib/widgets/weather_widget.dart | 165 ++++++++++++++++-- 6 files changed, 217 insertions(+), 33 deletions(-) diff --git a/backend/routes/weather.js b/backend/routes/weather.js index 7178b4a..1e08c19 100644 --- a/backend/routes/weather.js +++ b/backend/routes/weather.js @@ -25,9 +25,56 @@ router.get("/", async (req, res) => { params.q = q || city; } - const response = await axios.get(`${baseUrl}/weather`, { params }); - res.json(response.data); + // 1. Fetch Current Weather (to get lat/lon and timezone) + const currentResponse = await axios.get(`${baseUrl}/weather`, { params }); + const currentData = currentResponse.data; + const { coord } = currentData; + + // 2. Fetch Forecast (for daily min/max) & Air Quality + const [forecastResponse, airResponse] = await Promise.all([ + axios.get(`${baseUrl}/forecast`, { + params: { ...params, lat: coord.lat, lon: coord.lon }, + }), + axios.get(`${baseUrl}/air_pollution`, { + params: { lat: coord.lat, lon: coord.lon, appid: apiKey }, + }), + ]); + + // 3. Process Forecast for Today's Min/Max (Seoul Time: UTC+9) + // OpenWeatherMap returns timestamps in UTC. + // Seoul is UTC+9. + const SeoulOffset = 9 * 60 * 60 * 1000; + const nowKST = new Date(Date.now() + SeoulOffset); + const todayStr = nowKST.toISOString().split("T")[0]; // YYYY-MM-DD in KST + + const todayItems = forecastResponse.data.list.filter((item) => { + const itemDateKST = new Date(item.dt * 1000 + SeoulOffset); + const itemDateStr = itemDateKST.toISOString().split("T")[0]; + return itemDateStr === todayStr; + }); + + if (todayItems.length > 0) { + // Find min and max from the 3-hour segments + const minTemp = Math.min(...todayItems.map((item) => item.main.temp_min)); + const maxTemp = Math.max(...todayItems.map((item) => item.main.temp_max)); + + // Update the response structure + currentData.main.temp_min = minTemp; + currentData.main.temp_max = maxTemp; + } + + // 4. Attach Air Quality (AQI) + // OpenWeatherMap AQI: 1 (Good) ... 5 (Very Poor) + if (airResponse.data.list && airResponse.data.list.length > 0) { + currentData.aqi = airResponse.data.list[0].main.aqi; + } + + res.json(currentData); } catch (error) { + console.error("Weather API Error:", error.message); + if (error.response) { + console.error("Data:", error.response.data); + } res.status(500).json({ message: "Failed to fetch weather" }); } }); diff --git a/flutter_app/android/app/src/main/AndroidManifest.xml b/flutter_app/android/app/src/main/AndroidManifest.xml index a4f82cd..5db760f 100644 --- a/flutter_app/android/app/src/main/AndroidManifest.xml +++ b/flutter_app/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ +