106 lines
5.9 KiB
TypeScript
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 },
|
|
});
|