- **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.
83 lines
2.6 KiB
JavaScript
83 lines
2.6 KiB
JavaScript
const express = require("express");
|
|
const axios = require("axios");
|
|
const config = require("../config/api");
|
|
|
|
const router = express.Router();
|
|
|
|
router.get("/", async (req, res) => {
|
|
try {
|
|
const { apiKey, baseUrl, city, units, language } = config.weather;
|
|
if (!apiKey) {
|
|
return res.status(400).json({ message: "OPENWEATHER_API_KEY is not set" });
|
|
}
|
|
|
|
const { q, lat, lon } = req.query;
|
|
const params = {
|
|
appid: apiKey,
|
|
units,
|
|
lang: language,
|
|
};
|
|
|
|
if (lat && lon) {
|
|
params.lat = lat;
|
|
params.lon = lon;
|
|
} else {
|
|
params.q = q || city;
|
|
}
|
|
|
|
// 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" });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|