From b851a2f79b171d2fbcbae3604ce18c78e7360912 Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Tue, 2 Jun 2026 06:25:14 +0900 Subject: [PATCH] sync: update from workspace (latest ITSM/CICD/DR changes) --- app/(tabs)/_layout.tsx | 7 +++ app/(tabs)/insights.tsx | 131 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 app/(tabs)/insights.tsx diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 4088f3bb..c272dc86 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -84,6 +84,13 @@ export default function TabLayout() { tabBarIcon: ({ focused }) => , }} /> + , + }} + /> = { + CRITICAL: '#ef4444', WARNING: '#f59e0b', NORMAL: '#22c55e', NO_DATA: '#94a3b8', +} + +export default function InsightsScreen() { + const { token } = useAuth() + const [weekly, setWeekly] = useState(null) + const [anomaly, setAnomaly] = useState(null) + const [predict, setPredict] = useState(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 ( + + + + ) + + return ( + }> + {/* ์ด์ƒ ๊ฐ์ง€ ์•Œ๋ฆผ */} + {(anomaly?.anomalies?.length || 0) > 0 && ( + + โš ๏ธ ์ด์ƒ ๊ฐ์ง€ {anomaly!.anomalies.length}๊ฑด + {anomaly!.anomalies.map((a: any, i: number) => ( + {a.message} + ))} + + )} + + {/* ํ†ต๊ณ„ ์นด๋“œ */} + + {[ + { 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 => ( + + {item.val} + {item.label} + + ))} + + + {/* SLA ์˜ˆ์ธก */} + {predict && ( + + ๐Ÿ“‰ SLA ์œ„๋ฐ˜ ์˜ˆ์ธก (7์ผ) + + {Math.round((predict.breach_probability_7d || 0) * 100)}% + + ํ˜„์žฌ SLA: {predict.current_rate || 0}% + {predict.insight ? {predict.insight} : null} + + )} + + {/* AI ์ฃผ๊ฐ„ ์ธ์‚ฌ์ดํŠธ */} + {weekly?.ai_insight && ( + + ๐Ÿค– AI ์ฃผ๊ฐ„ ์ธ์‚ฌ์ดํŠธ + {weekly.ai_insight} + + )} + + {/* ์ƒ์œ„ ์นดํ…Œ๊ณ ๋ฆฌ */} + {(weekly?.top_categories?.length || 0) > 0 && ( + + ๐Ÿ“Š SR ์ƒ์œ„ ์นดํ…Œ๊ณ ๋ฆฌ + {weekly!.top_categories.map((c: any, i: number) => ( + + {c.category} + + + + {c.count}๊ฑด + + ))} + + )} + + ) +} + +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' }, +})