132 lines
6.0 KiB
TypeScript
132 lines
6.0 KiB
TypeScript
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' },
|
||
})
|