sync: update from workspace (latest ITSM/CICD/DR changes)

This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-02 06:25:14 +09:00
parent 3788b0f066
commit b851a2f79b
2 changed files with 138 additions and 0 deletions

View File

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