import { useEffect, useState, useCallback } from 'react' import { View, Text, ScrollView, TouchableOpacity, StyleSheet, RefreshControl, Alert, ActivityIndicator, } from 'react-native' import { COLORS } from '../../constants/Config' import { getDRDashboard, getDRRtoRpo, runDRTest } from '../../services/api' interface DRTest { test_id: number; test_type: string; status: string; started_at: string } interface DRDash { total_scenarios: number; pass_count: number; fail_count: number; untested_count: number; recent_tests: DRTest[] } interface RtoScen { scenario_id: number; scenario_name: string; rto_target: number | null; rto_actual_avg: number | null; rto_met: boolean | null; last_test_result: string | null } interface RtoRpo { scenarios: RtoScen[] } const STATUS_COLOR: Record = { PASS: '#22c55e', FAIL: '#ef4444', PARTIAL: '#f59e0b', RUNNING: '#4f6ef7', } export default function DRScreen() { const [dash, setDash] = useState(null) const [rto, setRto] = useState(null) const [loading, setLoading] = useState(true) const [refresh, setRefresh] = useState(false) const [running, setRunning] = useState(null) const load = useCallback(async (isRefresh = false) => { isRefresh ? setRefresh(true) : setLoading(true) try { const [d, r] = await Promise.all([getDRDashboard(), getDRRtoRpo()]) setDash(d.data); setRto(r.data) } catch { /* 네트워크 오류 무시 */ } finally { setLoading(false); setRefresh(false) } }, []) useEffect(() => { load() }, [load]) const handleTest = (scenarioId: number, name: string) => { Alert.alert('복구 테스트', `"${name}" 복구 테스트를 실행하시겠습니까?`, [ { text: '취소', style: 'cancel' }, { text: '실행', onPress: async () => { setRunning(scenarioId) try { const r = await runDRTest(scenarioId) Alert.alert('완료', `상태: ${r.data.status}\nRTO 실적: ${r.data.rto_actual_minutes ?? '-'}분`) load() } catch (e: any) { Alert.alert('오류', e.response?.data?.detail ?? '테스트 실행 실패') } finally { setRunning(null) } }}, ]) } if (loading) return ( ) const scenarios = rto?.scenarios ?? [] return ( load(true)} tintColor={COLORS.accent} />}> {/* 요약 카드 */} {[ { label: '전체', value: dash?.total_scenarios ?? 0, color: COLORS.text }, { label: 'PASS', value: dash?.pass_count ?? 0, color: '#22c55e' }, { label: 'FAIL', value: dash?.fail_count ?? 0, color: '#ef4444' }, { label: '미테스트', value: dash?.untested_count ?? 0, color: '#f59e0b' }, ].map(c => ( {c.value} {c.label} ))} {/* 시나리오 목록 */} DR 시나리오 (RTO/RPO) {scenarios.length === 0 ? 등록된 DR 시나리오가 없습니다. : scenarios.map(sc => ( {sc.scenario_name} {sc.last_test_result && ( {sc.last_test_result} )} RTO 목표: {sc.rto_target ? `${sc.rto_target}분` : '-'} 실적: {sc.rto_actual_avg != null ? `${sc.rto_actual_avg}분` : '기록없음'} {sc.rto_met === true && ✔ 충족} {sc.rto_met === false && ✘ 초과} handleTest(sc.scenario_id, sc.scenario_name)} disabled={running === sc.scenario_id}> {running === sc.scenario_id ? '실행 중...' : '복구 테스트 실행'} ))} {/* 최근 테스트 이력 */} 최근 테스트 이력 {(dash?.recent_tests ?? []).slice(0, 5).map(t => ( #{t.test_id} {t.test_type} {t.status} ))} ) } const s = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5f7fa' }, center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, row: { flexDirection: 'row', gap: 10, padding: 16 }, statCard: { flex: 1, backgroundColor: '#fff', borderRadius: 10, padding: 14, alignItems: 'center', shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 4, elevation: 2 }, statNum: { fontSize: 24, fontWeight: '700' }, statLabel: { fontSize: 11, color: '#94a3b8', marginTop: 4 }, sectionTitle: { fontSize: 13, fontWeight: '700', color: '#1e293b', paddingHorizontal: 16, marginBottom: 8, marginTop: 4 }, card: { backgroundColor: '#fff', borderRadius: 10, padding: 14, marginHorizontal: 16, marginBottom: 10, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 4, elevation: 2 }, cardRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }, cardName: { fontSize: 14, fontWeight: '600', color: '#1e293b', flex: 1 }, cardMeta: { flexDirection: 'row', gap: 12, marginBottom: 10 }, metaText: { fontSize: 12, color: '#64748b' }, badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10 }, badgeText: { fontSize: 10, fontWeight: '700', color: '#fff' }, btn: { backgroundColor: COLORS.accent, borderRadius: 8, padding: 10, alignItems: 'center' }, btnDisabled: { backgroundColor: '#e2e8f0' }, btnText: { color: '#fff', fontSize: 13, fontWeight: '600' }, empty: { color: '#94a3b8', textAlign: 'center', padding: 30, fontSize: 13 }, })