diff --git a/App.js b/App.js index 5b04146..cb3877f 100644 --- a/App.js +++ b/App.js @@ -14,6 +14,7 @@ import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import PlantDetailScreen from './screens/PlantDetailScreen'; +import ComparisonScreen from './screens/ComparisonScreen'; const API_URL = 'https://solorpower.dadot.net/plants/1'; const Stack = createNativeStackNavigator(); @@ -156,6 +157,12 @@ function DashboardScreen({ navigation }) { ☀️ 태양광 발전 현황 + navigation.navigate('Comparison')} + > + 📊 전체 비교 + 실시간 모니터링 대시보드 @@ -269,6 +276,7 @@ export default function App() { }} > + @@ -497,4 +505,18 @@ const styles = StyleSheet.create({ width: 40, textAlign: 'right', }, + compareButton: { + backgroundColor: 'rgba(255,255,255,0.2)', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 20, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.4)', + marginLeft: 12, + }, + compareButtonText: { + color: '#FFFFFF', + fontSize: 14, + fontWeight: '600', + }, }); diff --git a/screens/ComparisonScreen.js b/screens/ComparisonScreen.js new file mode 100644 index 0000000..d6b09bc --- /dev/null +++ b/screens/ComparisonScreen.js @@ -0,0 +1,432 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + StyleSheet, + View, + Text, + TouchableOpacity, + ActivityIndicator, + ScrollView, + useWindowDimensions, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { BarChart } from 'react-native-gifted-charts'; + +const API_BASE_URL = 'https://solorpower.dadot.net'; + +export default function ComparisonScreen({ navigation }) { + const [period, setPeriod] = useState('day'); + const [statsData, setStatsData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentDate, setCurrentDate] = useState(new Date()); + const { width } = useWindowDimensions(); + + useEffect(() => { + setCurrentDate(new Date()); + }, [period]); + + const moveDate = (direction) => { + const newDate = new Date(currentDate); + if (period === 'day') { + newDate.setDate(newDate.getDate() + direction); + } else if (period === 'month') { + newDate.setMonth(newDate.getMonth() + direction); + } else if (period === 'year') { + newDate.setFullYear(newDate.getFullYear() + direction); + } + setCurrentDate(newDate); + }; + + const formatDateDisplay = () => { + const y = currentDate.getFullYear(); + const m = currentDate.getMonth() + 1; + const d = currentDate.getDate(); + + if (period === 'day') return `${y}년 ${m}월 ${d}일`; + if (period === 'month') return `${y}년 ${m}월`; + if (period === 'year') return `${y}년`; + return ''; + }; + + const fetchStats = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const y = currentDate.getFullYear(); + const m = currentDate.getMonth() + 1; + const d = currentDate.getDate(); + const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`; + + let query = `period=${period}`; + if (period === 'day') { + query += `&date=${dateStr}`; + } else if (period === 'month') { + query += `&year=${y}&month=${m}`; + } else if (period === 'year') { + query += `&year=${y}`; + } + + const response = await fetch(`${API_BASE_URL}/plants/stats/comparison?${query}`); + if (!response.ok) throw new Error(`HTTP Error: ${response.status}`); + + const result = await response.json(); + setStatsData(result.data || []); + + } catch (err) { + setError(err.message); + setStatsData([]); + } finally { + setLoading(false); + } + }, [period, currentDate]); + + useEffect(() => { + fetchStats(); + }, [fetchStats]); + + // Chart Data Preparation + const chartData = statsData.map(item => { + let label = item.plant_name; + const match = label.match(/(\d+호기)/); + if (match) { + label = match[1]; + } else { + label = label.replace(/태양과바람|발전소/g, '').trim(); + } + + return { + value: item.generation, + label: label, + frontColor: '#3B82F6', + topLabelComponent: () => ( + + {item.generation.toFixed(0)} + + ), + }; + }); + + const renderTabs = () => ( + + {['day', 'month', 'year'].map((p) => ( + setPeriod(p)} + > + + {p === 'day' ? '일간' : p === 'month' ? '월간' : '연간'} + + + ))} + + ); + + const renderDateControl = () => ( + + moveDate(-1)} style={styles.arrowButton}> + + + {formatDateDisplay()} + moveDate(1)} style={styles.arrowButton}> + + + + ); + + const renderTable = () => ( + + + 발전소 + 발전량 + 용량 + 시간 + + {statsData.map((item, index) => ( + + {item.plant_name} + {item.generation.toFixed(1)} + {item.capacity} + + {item.generation_hours.toFixed(2)} + + + ))} + {statsData.length === 0 && ( + + 데이터가 없습니다 + + )} + + ); + + const chartWidth = width - 40; + + return ( + + + navigation.goBack()}> + ← 뒤로 + + 전체 발전소 비교 + + + + + {renderDateControl()} + {renderTabs()} + + {loading ? ( + + + 데이터 불러오는 중... + + ) : error ? ( + + 데이터 조회 실패 + {error} + + 다시 시도 + + + ) : ( + <> + + 📊 발전량 비교 (kWh) + + {statsData.length > 0 ? ( + + ) : ( + 표시할 데이터가 없습니다. + )} + + + + + + 📋 상세 현황 + 시간 = 발전량 / 설비용량 {period === 'year' ? '/ 365' : ''} + + {renderTable()} + + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F3F4F6', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: '#1E40AF', + padding: 16, + }, + backButton: { + padding: 8, + }, + backButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '500', + }, + headerTitle: { + fontSize: 18, + fontWeight: 'bold', + color: '#FFFFFF', + }, + content: { + flex: 1, + }, + scrollContent: { + padding: 16, + paddingBottom: 40, + }, + dateControl: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 16, + gap: 16, + }, + arrowButton: { + padding: 8, + }, + arrowText: { + fontSize: 20, + color: '#4B5563', + }, + dateText: { + fontSize: 18, + fontWeight: 'bold', + color: '#1F2937', + }, + tabContainer: { + flexDirection: 'row', + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 4, + marginBottom: 16, + shadowColor: '#000', + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + tab: { + flex: 1, + paddingVertical: 10, + alignItems: 'center', + borderRadius: 8, + }, + tabActive: { + backgroundColor: '#3B82F6', + }, + tabText: { + fontSize: 14, + fontWeight: '600', + color: '#6B7280', + }, + tabTextActive: { + color: '#FFFFFF', + }, + loadingContainer: { + height: 300, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 12, + color: '#6B7280', + }, + errorContainer: { + height: 300, + justifyContent: 'center', + alignItems: 'center', + }, + errorText: { + fontSize: 16, + color: '#EF4444', + marginBottom: 8, + }, + errorDetail: { + fontSize: 12, + color: '#6B7280', + marginBottom: 16, + }, + retryButton: { + backgroundColor: '#3B82F6', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + }, + retryButtonText: { + color: '#FFFFFF', + }, + chartSection: { + backgroundColor: '#FFFFFF', + borderRadius: 16, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 2, + }, + sectionTitle: { + fontSize: 16, + fontWeight: 'bold', + color: '#1F2937', + marginBottom: 12, + }, + chartContainer: { + alignItems: 'center', + paddingVertical: 10, + }, + noDataText: { + color: '#9CA3AF', + marginVertical: 20, + }, + tableSection: { + backgroundColor: '#FFFFFF', + borderRadius: 16, + padding: 16, + shadowColor: '#000', + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 2, + }, + tableHeaderRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + unitText: { + fontSize: 11, + color: '#6B7280', + }, + table: { + borderWidth: 1, + borderColor: '#E5E7EB', + borderRadius: 8, + overflow: 'hidden', + }, + tableHeader: { + flexDirection: 'row', + backgroundColor: '#F3F4F6', + paddingVertical: 10, + paddingHorizontal: 8, + borderBottomWidth: 1, + borderBottomColor: '#E5E7EB', + }, + columnHeader: { + fontSize: 12, + fontWeight: 'bold', + color: '#4B5563', + textAlign: 'center', + }, + tableRow: { + flexDirection: 'row', + paddingVertical: 12, + paddingHorizontal: 8, + borderBottomWidth: 1, + borderBottomColor: '#E5E7EB', + }, + tableRowAlt: { + backgroundColor: '#F9FAFB', + }, + cell: { + fontSize: 12, + color: '#1F2937', + textAlign: 'center', + }, + emptyRow: { + padding: 20, + alignItems: 'center', + }, + emptyText: { + color: '#9CA3AF', + fontSize: 14, + }, +});