guardia-messenger/app/(tabs)/predictive_alert.tsx

106 lines
5.9 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Switch } from 'react-native';
import { ITSM_BASE } from '../../services/api';
interface PredictiveAlert { id: string; type: string; title: string; detail: string; probability: number; eta_min: number; severity: string; auto_action?: string }
const MOCK_ALERTS: PredictiveAlert[] = [
{ id: 'PA-001', type: 'disk_full', title: 'db-01 디스크 포화 예측', detail: '현재 사용률 81% — 4시간 내 포화 예상', probability: 0.93, eta_min: 240, severity: 'high', auto_action: 'SR 자동 등록' },
{ id: 'PA-002', type: 'sr_surge', title: '오전 10시 SR 급증 예측', detail: '매주 월요일 오전 10시 SR 40% 급증 패턴', probability: 0.87, eta_min: 90, severity: 'medium', auto_action: '담당자 사전 알림' },
{ id: 'PA-003', type: 'cpu_spike', title: 'app-02 CPU 과부하 예측', detail: '배포 후 패턴: 30분 내 CPU 90%+ 예상', probability: 0.72, eta_min: 30, severity: 'medium' },
];
export default function PredictiveAlertScreen() {
const [alerts, setAlerts] = useState<PredictiveAlert[]>(MOCK_ALERTS);
const [autoAction, setAutoAction] = useState(true);
const [smartFilter, setSmartFilter] = useState(true);
const [loading, setLoading] = useState(false);
const fetchPredictions = async () => {
setLoading(true);
try {
const r = await fetch(`${ITSM_BASE}/api/failure-prevention/predictions`);
if (r.ok) { const d = await r.json(); if (d.predictions?.length) setAlerts(d.predictions); }
} catch {}
setLoading(false);
};
useEffect(() => { fetchPredictions(); }, []);
const severityColor = (s: string) => ({ high: '#ff8800', medium: '#ffbb00', critical: '#ff4444', low: '#44bb44' })[s] || '#888';
const applyAction = async (alert: PredictiveAlert) => {
try {
await fetch(`${ITSM_BASE}/api/tasks`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: `[예측알림] ${alert.title}`, description: alert.detail, priority: alert.severity }),
});
setAlerts(prev => prev.filter(a => a.id !== alert.id));
} catch {}
};
return (
<ScrollView style={s.container}>
<Text style={s.title}> </Text>
<Text style={s.sub}>AI가 </Text>
<View style={s.settingsCard}>
<View style={s.settingRow}>
<Text style={s.settingLabel}> </Text>
<Switch value={autoAction} onValueChange={setAutoAction} trackColor={{ true: '#00A0C8', false: '#333' }} />
</View>
<View style={s.settingRow}>
<Text style={s.settingLabel}> ( )</Text>
<Switch value={smartFilter} onValueChange={setSmartFilter} trackColor={{ true: '#00A0C8', false: '#333' }} />
</View>
</View>
<View style={s.summaryRow}>
<View style={s.summaryItem}><Text style={s.summaryVal}>{alerts.length}</Text><Text style={s.summaryLbl}> </Text></View>
<View style={s.summaryItem}><Text style={s.summaryVal}>{alerts.filter(a => a.severity === 'high' || a.severity === 'critical').length}</Text><Text style={s.summaryLbl}> </Text></View>
<View style={s.summaryItem}><Text style={s.summaryVal}>{alerts.filter(a => a.auto_action).length}</Text><Text style={s.summaryLbl}> </Text></View>
</View>
{(smartFilter ? alerts.filter(a => a.probability >= 0.7) : alerts).map(alert => (
<View key={alert.id} style={[s.alertCard, { borderLeftColor: severityColor(alert.severity) }]}>
<View style={s.alertHeader}>
<View style={[s.badge, { backgroundColor: severityColor(alert.severity) }]}>
<Text style={s.badgeText}>{Math.round(alert.probability * 100)}%</Text>
</View>
<Text style={s.etaText}> {alert.eta_min} </Text>
</View>
<Text style={s.alertTitle}>{alert.title}</Text>
<Text style={s.alertDetail}>{alert.detail}</Text>
{alert.auto_action && (
<TouchableOpacity style={s.actionBtn} onPress={() => applyAction(alert)}>
<Text style={s.actionBtnText}> {alert.auto_action}</Text>
</TouchableOpacity>
)}
</View>
))}
</ScrollView>
);
}
const s = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0A0E1A', padding: 16 },
title: { color: '#fff', fontSize: 20, fontWeight: '700', marginBottom: 4 },
sub: { color: '#888', fontSize: 13, marginBottom: 16 },
settingsCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 16, borderWidth: 1, borderColor: '#333' },
settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
settingLabel: { color: '#fff', fontSize: 14 },
summaryRow: { flexDirection: 'row', justifyContent: 'space-around', backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 16, borderWidth: 1, borderColor: '#333' },
summaryItem: { alignItems: 'center' },
summaryVal: { color: '#00A0C8', fontSize: 22, fontWeight: '700' },
summaryLbl: { color: '#888', fontSize: 11, marginTop: 2 },
alertCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 12, borderLeftWidth: 4, borderWidth: 1, borderColor: '#333' },
alertHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 },
badge: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 6 },
badgeText: { color: '#fff', fontWeight: '700', fontSize: 12 },
etaText: { color: '#888', fontSize: 12 },
alertTitle: { color: '#fff', fontWeight: '700', fontSize: 15, marginBottom: 6 },
alertDetail: { color: '#aaa', fontSize: 13, marginBottom: 10 },
actionBtn: { backgroundColor: '#003366', padding: 10, borderRadius: 8, borderWidth: 1, borderColor: '#00A0C8' },
actionBtnText: { color: '#fff', fontWeight: '600', fontSize: 13 },
});