sync: update from workspace (latest ITSM/CICD/DR changes)
This commit is contained in:
parent
3788b0f066
commit
b851a2f79b
@ -84,6 +84,13 @@ export default function TabLayout() {
|
||||
tabBarIcon: ({ focused }) => <TabIcon icon="🔀" label="네트워크" focused={focused} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="insights"
|
||||
options={{
|
||||
title: 'AI 인사이트',
|
||||
tabBarIcon: ({ focused }) => <TabIcon icon="🧠" label="AI" focused={focused} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
|
||||
131
app/(tabs)/insights.tsx
Normal file
131
app/(tabs)/insights.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, ActivityIndicator, RefreshControl } from 'react-native'
|
||||
import { COLORS } from '../../constants/Config'
|
||||
import { apiClient } from '../../services/api'
|
||||
import { useAuth } from '../../hooks/useAuth'
|
||||
|
||||
interface Weekly { stats: any; ai_insight: string; top_categories: any[] }
|
||||
interface Anomaly { anomalies: any[]; today_sr: number; avg_7d: number; open_sr: number }
|
||||
interface Predict { breach_probability_7d: number; current_rate: number; status: string; insight: string }
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
CRITICAL: '#ef4444', WARNING: '#f59e0b', NORMAL: '#22c55e', NO_DATA: '#94a3b8',
|
||||
}
|
||||
|
||||
export default function InsightsScreen() {
|
||||
const { token } = useAuth()
|
||||
const [weekly, setWeekly] = useState<Weekly | null>(null)
|
||||
const [anomaly, setAnomaly] = useState<Anomaly | null>(null)
|
||||
const [predict, setPredict] = useState<Predict | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [w, a, p] = await Promise.all([
|
||||
apiClient.get('/api/insights/weekly', token),
|
||||
apiClient.get('/api/insights/anomalies', token),
|
||||
apiClient.get('/api/predict/sla-breach', token),
|
||||
])
|
||||
setWeekly(w); setAnomaly(a); setPredict(p)
|
||||
} catch { /* 오류 무시 */ }
|
||||
setLoading(false); setRefreshing(false)
|
||||
}
|
||||
|
||||
function onRefresh() { setRefreshing(true); load() }
|
||||
|
||||
if (loading) return (
|
||||
<View style={s.center}>
|
||||
<ActivityIndicator color={COLORS.accent} size="large" />
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollView style={s.scroll} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={COLORS.accent} />}>
|
||||
{/* 이상 감지 알림 */}
|
||||
{(anomaly?.anomalies?.length || 0) > 0 && (
|
||||
<View style={s.alertCard}>
|
||||
<Text style={s.alertTitle}>⚠️ 이상 감지 {anomaly!.anomalies.length}건</Text>
|
||||
{anomaly!.anomalies.map((a: any, i: number) => (
|
||||
<Text key={i} style={s.alertMsg}>{a.message}</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 통계 카드 */}
|
||||
<View style={s.statsRow}>
|
||||
{[
|
||||
{ label: '신규 SR', val: weekly?.stats?.total || 0, color: COLORS.primary },
|
||||
{ label: '완료율', val: `${weekly?.stats?.completion_rate || 0}%`, color: '#22c55e' },
|
||||
{ label: '미처리', val: weekly?.stats?.open || 0, color: '#ef4444' },
|
||||
].map(item => (
|
||||
<View key={item.label} style={[s.statCard, { borderTopColor: item.color }]}>
|
||||
<Text style={[s.statVal, { color: item.color }]}>{item.val}</Text>
|
||||
<Text style={s.statLabel}>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* SLA 예측 */}
|
||||
{predict && (
|
||||
<View style={[s.card, { borderLeftColor: STATUS_COLOR[predict.status] || '#94a3b8', borderLeftWidth: 4 }]}>
|
||||
<Text style={s.cardTitle}>📉 SLA 위반 예측 (7일)</Text>
|
||||
<Text style={[s.bigNum, { color: STATUS_COLOR[predict.status] || '#94a3b8' }]}>
|
||||
{Math.round((predict.breach_probability_7d || 0) * 100)}%
|
||||
</Text>
|
||||
<Text style={s.subText}>현재 SLA: {predict.current_rate || 0}%</Text>
|
||||
{predict.insight ? <Text style={s.insightText}>{predict.insight}</Text> : null}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* AI 주간 인사이트 */}
|
||||
{weekly?.ai_insight && (
|
||||
<View style={s.card}>
|
||||
<Text style={s.cardTitle}>🤖 AI 주간 인사이트</Text>
|
||||
<Text style={s.insightText}>{weekly.ai_insight}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 상위 카테고리 */}
|
||||
{(weekly?.top_categories?.length || 0) > 0 && (
|
||||
<View style={s.card}>
|
||||
<Text style={s.cardTitle}>📊 SR 상위 카테고리</Text>
|
||||
{weekly!.top_categories.map((c: any, i: number) => (
|
||||
<View key={i} style={s.categoryRow}>
|
||||
<Text style={s.categoryName}>{c.category}</Text>
|
||||
<View style={s.barBg}>
|
||||
<View style={[s.barFill, { width: `${Math.min(100, c.count * 5)}%` as any }]} />
|
||||
</View>
|
||||
<Text style={s.categoryCount}>{c.count}건</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const s = StyleSheet.create({
|
||||
scroll: { flex: 1, backgroundColor: '#f8fafc' },
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
alertCard: { margin: 12, padding: 14, backgroundColor: '#fef2f2', borderRadius: 12, borderLeftWidth: 4, borderLeftColor: '#ef4444' },
|
||||
alertTitle: { fontWeight: '700', fontSize: 14, color: '#7f1d1d', marginBottom: 4 },
|
||||
alertMsg: { fontSize: 12, color: '#991b1b', marginTop: 3 },
|
||||
statsRow: { flexDirection: 'row', padding: 12, gap: 8 },
|
||||
statCard: { flex: 1, backgroundColor: '#fff', borderRadius: 12, padding: 14, alignItems: 'center', borderTopWidth: 3, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 4, elevation: 2 },
|
||||
statVal: { fontSize: 24, fontWeight: '800' },
|
||||
statLabel: { fontSize: 11, color: '#64748b', marginTop: 2 },
|
||||
card: { margin: 12, marginTop: 0, backgroundColor: '#fff', borderRadius: 12, padding: 16, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 4, elevation: 2 },
|
||||
cardTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b', marginBottom: 10 },
|
||||
bigNum: { fontSize: 36, fontWeight: '800' },
|
||||
subText: { fontSize: 12, color: '#64748b', marginTop: 2 },
|
||||
insightText: { fontSize: 13, lineHeight: 20, color: '#374151', marginTop: 8 },
|
||||
categoryRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 },
|
||||
categoryName: { fontSize: 12, width: 80, color: '#374151' },
|
||||
barBg: { flex: 1, backgroundColor: '#f1f5f9', borderRadius: 3, height: 6 },
|
||||
barFill: { height: 6, backgroundColor: COLORS.primary, borderRadius: 3 },
|
||||
categoryCount:{ fontSize: 11, color: '#64748b', width: 30, textAlign: 'right' },
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user