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',
},
});