guardia-messenger/app/(tabs)/insights.tsx
2026-06-02 06:25:14 +09:00

132 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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