From 8a1887a26d5adb09713ebc6d6525e123ffcfe36d Mon Sep 17 00:00:00 2001 From: Akira Date: Sat, 28 Feb 2026 13:40:52 +0900 Subject: [PATCH] =?UTF-8?q?=E6=B0=97=E8=B1=A1=E3=83=87=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=82=92=E8=BF=BD=E5=8A=A0=EF=BC=88=E6=9C=88?= =?UTF-8?q?=E5=88=A5=E3=82=B5=E3=83=9E=E3=83=AA=E3=83=BC=E3=83=BB=E7=9B=B4?= =?UTF-8?q?=E8=BF=9114=E6=97=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /weather ページ: 月別集計テーブル・年間サマリーカード・直近14日日次記録 - Navbar に「気象」リンク追加 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/app/weather/page.tsx | 310 +++++++++++++++++++++++++++++ frontend/src/components/Navbar.tsx | 13 +- 2 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/weather/page.tsx diff --git a/frontend/src/app/weather/page.tsx b/frontend/src/app/weather/page.tsx new file mode 100644 index 0000000..6fb8a87 --- /dev/null +++ b/frontend/src/app/weather/page.tsx @@ -0,0 +1,310 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { api } from '@/lib/api'; +import Navbar from '@/components/Navbar'; +import { Cloud, Loader2, Sun, Droplets, Wind, Thermometer } from 'lucide-react'; + +interface MonthlyRecord { + month: number; + temp_mean_avg: number | null; + temp_max_avg: number | null; + temp_min_avg: number | null; + precip_total: number | null; + sunshine_total: number | null; + wind_max: number | null; + hot_days: number; + cold_days: number; + rainy_days: number; +} + +interface AnnualSummary { + temp_mean_avg: number | null; + precip_total: number | null; + sunshine_total: number | null; + hot_days: number; + cold_days: number; +} + +interface SummaryResponse { + year: number; + monthly: MonthlyRecord[]; + annual: AnnualSummary; +} + +interface DailyRecord { + date: string; + temp_mean: number | null; + temp_max: number | null; + temp_min: number | null; + sunshine_h: number | null; + precip_mm: number | null; + wind_max: number | null; + pressure_min: number | null; +} + +const MONTHS = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; + +function fmt(val: number | null, digits = 1): string { + return val == null ? '—' : val.toFixed(digits); +} + +export default function WeatherPage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(currentYear); + const [tab, setTab] = useState<'summary' | 'recent'>('summary'); + + const [summary, setSummary] = useState(null); + const [summaryLoading, setSummaryLoading] = useState(true); + + const [recentRecords, setRecentRecords] = useState([]); + const [recentLoading, setRecentLoading] = useState(true); + + useEffect(() => { + setSummaryLoading(true); + api.get(`/weather/summary/?year=${year}`) + .then((res) => setSummary(res.data)) + .catch((err) => console.error(err)) + .finally(() => setSummaryLoading(false)); + }, [year]); + + useEffect(() => { + setRecentLoading(true); + const end = new Date(); + end.setDate(end.getDate() - 1); + const start = new Date(end); + start.setDate(start.getDate() - 13); + const fmt2 = (d: Date) => d.toISOString().slice(0, 10); + api.get(`/weather/records/?start=${fmt2(start)}&end=${fmt2(end)}`) + .then((res) => { + const data: DailyRecord[] = res.data; + setRecentRecords([...data].reverse()); + }) + .catch((err) => console.error(err)) + .finally(() => setRecentLoading(false)); + }, []); + + const years: number[] = []; + for (let y = currentYear; y >= 2016; y--) years.push(y); + + return ( +
+ +
+ + {/* ヘッダー */} +
+
+ +

気象データ

+
+
+ +
+
+ + {/* タブ */} +
+ + +
+ + {/* ===== 月別サマリー ===== */} + {tab === 'summary' && ( + summaryLoading ? ( +
+ +
+ ) : summary ? ( +
+ {/* 年間サマリーカード */} +
+
+
+ +

年間平均気温

+
+

+ {fmt(summary.annual.temp_mean_avg)} +

+
+
+
+ +

年間降水量

+
+

+ {fmt(summary.annual.precip_total, 0)}mm +

+
+
+
+ +

年間日照時間

+
+

+ {fmt(summary.annual.sunshine_total, 0)}h +

+
+
+
+ +

猛暑日 / 冬日

+
+

+ {summary.annual.hot_days}猛暑 + / + {summary.annual.cold_days} +

+
+
+ + {/* 月別テーブル */} +
+
+ + + + + + + + + + + + + + + + {summary.monthly.map((m) => ( + + + + + + + + + + + + ))} + +
平均気温最高(avg)最低(avg)日照h降水mm猛暑日冬日雨天日
{MONTHS[m.month - 1]} + {m.temp_mean_avg == null ? '—' : ( + = 25 ? 'text-orange-500 font-medium' : m.temp_mean_avg < 5 ? 'text-blue-500 font-medium' : ''}> + {fmt(m.temp_mean_avg)}℃ + + )} + {fmt(m.temp_max_avg)}℃{fmt(m.temp_min_avg)}℃{fmt(m.sunshine_total, 0)}{fmt(m.precip_total, 0)} + {m.hot_days > 0 ? {m.hot_days} : 0} + + {m.cold_days > 0 ? {m.cold_days} : 0} + {m.rainy_days}
+
+
+ 猛暑日: 最高気温 ≥ 35℃ 冬日: 最低気温 < 0℃ 雨天日: 降水量 ≥ 1mm +
+
+
+ ) : ( +
データの取得に失敗しました
+ ) + )} + + {/* ===== 直近14日 ===== */} + {tab === 'recent' && ( + recentLoading ? ( +
+ +
+ ) : ( +
+
+ + + + + + + + + + + + + + + {recentRecords.length === 0 ? ( + + + + ) : ( + recentRecords.map((r) => ( + + + + + + + + + + + )) + )} + +
日付平均気温最高最低日照h降水mm最大風速最低気圧
データがありません
{r.date}{fmt(r.temp_mean)}℃ + = 35 ? 'text-red-500 font-medium' : 'text-gray-500'}> + {fmt(r.temp_max)}℃ + + + + {fmt(r.temp_min)}℃ + + {fmt(r.sunshine_h)} + = 10 ? 'text-blue-600 font-medium' : 'text-gray-700'}> + {fmt(r.precip_mm)} + + {fmt(r.wind_max)}{fmt(r.pressure_min, 0)}
+
+ {recentRecords.length > 0 && ( +
+ 最新: {recentRecords[0].date} 全{recentRecords.length}件表示 +
+ )} +
+ ) + )} + +
+
+ ); +} diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 9bcc139..832a27c 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRouter, usePathname } from 'next/navigation'; -import { LogOut, Wheat, MapPin, FileText, Upload, LayoutDashboard, Mail, History, Shield, KeyRound } from 'lucide-react'; +import { LogOut, Wheat, MapPin, FileText, Upload, LayoutDashboard, Mail, History, Shield, KeyRound, Cloud } from 'lucide-react'; import { logout } from '@/lib/api'; export default function Navbar() { @@ -100,6 +100,17 @@ export default function Navbar() { メールルール +