zioinfo-mail/workspace/guardia-messenger/app/(tabs)/insights.tsx
DESKTOP-TKLFCPR\ython caa70a608b feat(frontend): 나머지 개발 — ITSM/Manager/Messenger 신규 UI
ITSM static (app.js + index.html):
- 사이드바: AI플랫폼·분석KPI·클라우드·외부연동·SaaS 5개 그룹 추가
- 23개 신규 뷰 핸들러 (rag_search, kpi_dashboard, bi_dashboard, jira_sync 등)
- 액션 헬퍼 함수 20개+ (재계산, 파인튜닝, 보고서 생성, 데이터 기여 등)

Manager 5개 신규 페이지:
- KpiDashboard.tsx: KPI 신호등 대시보드 + 재계산
- BiAnalytics.tsx: SR트렌드·카테고리파이·MTTR·엔지니어워크로드
- BillingManage.tsx: 구독플랜·사용량·청구서 이력
- IntegrationHub.tsx: Jira/Slack/ServiceNow/ERP/Kakao/SSO 탭
- AiPlatform.tsx: AI인사이트·LearningLoop·예측·벤치마킹

Messenger 신규 탭:
- insights.tsx: AI 주간 인사이트 + SLA 예측 + 이상 감지

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 06:24:54 +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' },
})