import React, { useState, useEffect, useCallback } from 'react'; import { StyleSheet, View, Text, ScrollView, TouchableOpacity, ActivityIndicator, useWindowDimensions, RefreshControl, } from 'react-native'; import { StatusBar } from 'expo-status-bar'; 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'; const API_URL = 'https://solorpower.dadot.net/plants/1'; const Stack = createNativeStackNavigator(); // 메인 대시보드 화면 function DashboardScreen({ navigation }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const { width } = useWindowDimensions(); const isPC = width >= 768; const fetchData = useCallback(async () => { try { setError(null); const response = await fetch(API_URL); if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); setRefreshing(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); const onRefresh = useCallback(() => { setRefreshing(true); fetchData(); }, [fetchData]); // 발전소 상태 및 아이콘 결정 const getStatusInfo = (plant) => { const latestLog = plant.latest_log; const currentKw = latestLog?.current_kw || 0; const status = latestLog?.status; if (status === '오류' || status === 'error') { return { icon: '🔴', label: '오류', color: '#EF4444' }; } // 야간 시간대 확인 (18시 ~ 06시) const hour = new Date().getHours(); const isNight = hour >= 18 || hour < 6; if (currentKw === 0) { if (isNight) { return { icon: '🌙', label: '야간', color: '#4B5563' }; } return { icon: '💤', label: '대기', color: '#6B7280' }; } return { icon: '🟢', label: status || '정상', color: '#10B981' }; }; // 요약 데이터 계산 const getSummary = () => { if (!data?.data || data.data.length === 0) { return { totalOutput: 0, operatingRate: 0, totalPlants: 0, activePlants: 0 }; } const plants = data.data; const totalPlants = plants.length; let totalOutput = 0; let activePlants = 0; plants.forEach((plant) => { const currentKw = plant.latest_log?.current_kw || 0; totalOutput += currentKw; if (currentKw > 0) { activePlants++; } }); const operatingRate = totalPlants > 0 ? (activePlants / totalPlants) * 100 : 0; return { totalOutput, operatingRate, totalPlants, activePlants }; }; // 로딩 상태 if (loading) { return ( 데이터를 불러오는 중... ); } // 에러 상태 if (error) { return ( ⚠️ 오류가 발생했습니다 {error} 다시 시도 ); } const summary = getSummary(); // 발전소 이름의 숫자 기준으로 정렬 (예: "태양과바람 1호기" -> 1) const plants = [...(data?.data || [])].sort((a, b) => { const getNumber = (str) => { // 문자열에서 첫 번째 연속된 숫자를 추출 const match = str && str.match(/(\d+)/); return match ? parseInt(match[1], 10) : Infinity; }; const numA = getNumber(a.name); const numB = getNumber(b.name); if (numA !== numB) return numA - numB; return (a.name || '').localeCompare(b.name || ''); }); // 발전소 카드 클릭 핸들러 const handlePlantPress = (plant) => { navigation.navigate('PlantDetail', { plant }); }; return ( {/* 상단 헤더 */} ☀️ 태양광 발전 현황 실시간 모니터링 대시보드 {/* 요약 카드 */} 총 출력 {summary.totalOutput.toFixed(1)} kW 가동률 {summary.operatingRate.toFixed(0)} % 가동 현황 {summary.activePlants}/{summary.totalPlants} 발전소 {/* 발전소 목록 */} } > {plants.map((plant, index) => { const statusInfo = getStatusInfo(plant); const latestLog = plant.latest_log; const currentKw = latestLog?.current_kw || 0; const dailyKwh = latestLog?.today_kwh || 0; return ( handlePlantPress(plant)} activeOpacity={0.7} > {statusInfo.icon} {plant.name} {statusInfo.label} 현재 출력 {currentKw.toFixed(1)} kW 금일 발전 {dailyKwh.toFixed(1)} kWh {plant.capacity && ( 설비 용량 {plant.capacity} kW )} {/* 진행률 바 */} {plant.capacity && ( {((currentKw / plant.capacity) * 100).toFixed(0)}% )} ); })} ); } // 메인 앱 export default function App() { return ( ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F3F4F6', }, centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F3F4F6', padding: 20, }, loadingText: { marginTop: 16, fontSize: 16, color: '#6B7280', }, errorIcon: { fontSize: 48, marginBottom: 16, }, errorText: { fontSize: 18, fontWeight: '600', color: '#1F2937', marginBottom: 8, }, errorDetail: { fontSize: 14, color: '#6B7280', marginBottom: 24, textAlign: 'center', }, retryButton: { backgroundColor: '#3B82F6', paddingHorizontal: 32, paddingVertical: 12, borderRadius: 8, }, retryButtonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600', }, // Header header: { backgroundColor: '#1E40AF', paddingTop: 60, paddingBottom: 24, paddingHorizontal: 20, }, headerTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, headerTitle: { fontSize: 24, fontWeight: 'bold', color: '#FFFFFF', marginBottom: 4, }, headerSubtitle: { fontSize: 14, color: '#93C5FD', }, // Summary summaryContainer: { flexDirection: 'row', paddingHorizontal: 16, paddingVertical: 16, gap: 12, }, summaryContainerPC: { maxWidth: 800, alignSelf: 'center', width: '100%', }, summaryCard: { flex: 1, borderRadius: 12, padding: 16, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, summaryCardPrimary: { backgroundColor: '#3B82F6', }, summaryCardSecondary: { backgroundColor: '#10B981', }, summaryCardTertiary: { backgroundColor: '#8B5CF6', }, summaryLabel: { fontSize: 12, color: 'rgba(255,255,255,0.8)', marginBottom: 4, }, summaryValue: { fontSize: 28, fontWeight: 'bold', color: '#FFFFFF', }, summaryUnit: { fontSize: 12, color: 'rgba(255,255,255,0.8)', marginTop: 2, }, // Cards scrollView: { flex: 1, }, cardContainer: { padding: 16, gap: 12, }, cardContainerPC: { flexDirection: 'row', flexWrap: 'wrap', maxWidth: 1200, alignSelf: 'center', width: '100%', gap: 16, }, card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 4, }, cardPC: { width: 'calc(33.333% - 11px)', minWidth: 280, }, cardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, }, statusIcon: { fontSize: 32, marginRight: 12, }, cardTitleContainer: { flex: 1, }, cardTitle: { fontSize: 18, fontWeight: '700', color: '#1F2937', marginBottom: 2, }, statusLabel: { fontSize: 12, fontWeight: '600', }, arrowIcon: { fontSize: 24, color: '#9CA3AF', fontWeight: '300', }, cardBody: { gap: 8, }, dataRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, dataLabel: { fontSize: 14, color: '#6B7280', }, dataValue: { fontSize: 14, fontWeight: '500', color: '#374151', }, dataValuePrimary: { fontSize: 20, fontWeight: '700', color: '#3B82F6', }, // Progress Bar progressContainer: { flexDirection: 'row', alignItems: 'center', marginTop: 16, gap: 8, }, progressBar: { flex: 1, height: 8, backgroundColor: '#E5E7EB', borderRadius: 4, overflow: 'hidden', }, progressFill: { height: '100%', borderRadius: 4, }, progressText: { fontSize: 12, fontWeight: '600', color: '#6B7280', width: 40, textAlign: 'right', }, });