433 lines
14 KiB
JavaScript
433 lines
14 KiB
JavaScript
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: () => (
|
|
<Text style={{ color: '#6B7280', fontSize: 10, marginBottom: 4 }}>
|
|
{item.generation.toFixed(0)}
|
|
</Text>
|
|
),
|
|
};
|
|
});
|
|
|
|
const renderTabs = () => (
|
|
<View style={styles.tabContainer}>
|
|
{['day', 'month', 'year'].map((p) => (
|
|
<TouchableOpacity
|
|
key={p}
|
|
style={[styles.tab, period === p && styles.tabActive]}
|
|
onPress={() => setPeriod(p)}
|
|
>
|
|
<Text style={[styles.tabText, period === p && styles.tabTextActive]}>
|
|
{p === 'day' ? '일간' : p === 'month' ? '월간' : '연간'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
);
|
|
|
|
const renderDateControl = () => (
|
|
<View style={styles.dateControl}>
|
|
<TouchableOpacity onPress={() => moveDate(-1)} style={styles.arrowButton}>
|
|
<Text style={styles.arrowText}>◀</Text>
|
|
</TouchableOpacity>
|
|
<Text style={styles.dateText}>{formatDateDisplay()}</Text>
|
|
<TouchableOpacity onPress={() => moveDate(1)} style={styles.arrowButton}>
|
|
<Text style={styles.arrowText}>▶</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
|
|
const renderTable = () => (
|
|
<View style={styles.table}>
|
|
<View style={styles.tableHeader}>
|
|
<Text style={[styles.columnHeader, { flex: 2 }]}>발전소</Text>
|
|
<Text style={[styles.columnHeader, { flex: 1.5 }]}>발전량</Text>
|
|
<Text style={[styles.columnHeader, { flex: 1.5 }]}>용량</Text>
|
|
<Text style={[styles.columnHeader, { flex: 1.5 }]}>시간</Text>
|
|
</View>
|
|
{statsData.map((item, index) => (
|
|
<View key={item.plant_id} style={[styles.tableRow, index % 2 === 1 && styles.tableRowAlt]}>
|
|
<Text style={[styles.cell, { flex: 2, textAlign: 'left' }]}>{item.plant_name}</Text>
|
|
<Text style={[styles.cell, { flex: 1.5 }]}>{item.generation.toFixed(1)}</Text>
|
|
<Text style={[styles.cell, { flex: 1.5, color: '#9CA3AF' }]}>{item.capacity}</Text>
|
|
<Text style={[styles.cell, { flex: 1.5, fontWeight: 'bold', color: '#10B981' }]}>
|
|
{item.generation_hours.toFixed(2)}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
{statsData.length === 0 && (
|
|
<View style={styles.emptyRow}>
|
|
<Text style={styles.emptyText}>데이터가 없습니다</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
|
|
const chartWidth = width - 40;
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
|
|
<Text style={styles.backButtonText}>← 뒤로</Text>
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>전체 발전소 비교</Text>
|
|
<View style={{ width: 60 }} />
|
|
</View>
|
|
|
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
|
{renderDateControl()}
|
|
{renderTabs()}
|
|
|
|
{loading ? (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color="#3B82F6" />
|
|
<Text style={styles.loadingText}>데이터 불러오는 중...</Text>
|
|
</View>
|
|
) : error ? (
|
|
<View style={styles.errorContainer}>
|
|
<Text style={styles.errorText}>데이터 조회 실패</Text>
|
|
<Text style={styles.errorDetail}>{error}</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={fetchStats}>
|
|
<Text style={styles.retryButtonText}>다시 시도</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<View style={styles.chartSection}>
|
|
<Text style={styles.sectionTitle}>📊 발전량 비교 (kWh)</Text>
|
|
<View style={styles.chartContainer}>
|
|
{statsData.length > 0 ? (
|
|
<BarChart
|
|
data={chartData}
|
|
width={chartWidth}
|
|
height={200}
|
|
barWidth={20}
|
|
spacing={15}
|
|
barBorderRadius={4}
|
|
frontColor="#3B82F6"
|
|
yAxisThickness={0}
|
|
xAxisThickness={1}
|
|
yAxisTextStyle={{ color: '#9CA3AF', fontSize: 10 }}
|
|
xAxisLabelTextStyle={{ color: '#6B7280', fontSize: 10 }}
|
|
hideRules
|
|
showValuesAsTopLabel
|
|
/>
|
|
) : (
|
|
<Text style={styles.noDataText}>표시할 데이터가 없습니다.</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.tableSection}>
|
|
<View style={styles.tableHeaderRow}>
|
|
<Text style={styles.sectionTitle}>📋 상세 현황</Text>
|
|
<Text style={styles.unitText}>시간 = 발전량 / 설비용량 {period === 'year' ? '/ 365' : ''}</Text>
|
|
</View>
|
|
{renderTable()}
|
|
</View>
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
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,
|
|
},
|
|
});
|