feat: Enhance TV support and Daily Weather Forecast (w/ Air Quality)

- **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.
This commit is contained in:
kihong.kim
2026-01-31 21:19:13 +09:00
parent 6fb00fec5d
commit e2f00c6e21
6 changed files with 217 additions and 33 deletions

View File

@@ -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" });
}
});