77 lines
3.6 KiB
TypeScript
77 lines
3.6 KiB
TypeScript
import React, { useState, useCallback } from 'react'
|
|
import { View, Text, ScrollView, StyleSheet, RefreshControl } from 'react-native'
|
|
import { useFocusEffect } from 'expo-router'
|
|
import { COLORS } from '../../constants/Config'
|
|
import client from '../../services/api'
|
|
|
|
export default function SecurityScoreScreen() {
|
|
const [data, setData] = useState<any>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
try { const r = await client.get('/api/ai-soc/security-score'); setData(r.data) }
|
|
catch { setData(null) } finally { setLoading(false) }
|
|
}, [])
|
|
|
|
useFocusEffect(useCallback(() => { load() }, [load]))
|
|
|
|
if (!data) return <Text style={s.empty}>보안 점수를 불러올 수 없습니다.</Text>
|
|
|
|
const score = data.total_score ?? data.score ?? 0
|
|
const color = score >= 80 ? COLORS.success : score >= 60 ? COLORS.warning : COLORS.danger
|
|
|
|
const domains = data.domains ?? [
|
|
{ name: 'Zero Trust 정책', score: data.zt_score ?? 0 },
|
|
{ name: '취약점 관리', score: data.vuln_score ?? 0 },
|
|
{ name: '감사 로그 완전성', score: data.audit_score ?? 0 },
|
|
{ name: '패치 적용률', score: data.patch_score ?? 0 },
|
|
{ name: 'CSAP 준수', score: data.csap_score ?? 0 },
|
|
]
|
|
|
|
return (
|
|
<ScrollView style={s.container} refreshControl={<RefreshControl refreshing={loading} onRefresh={load} />} contentContainerStyle={{ padding: 16 }}>
|
|
<View style={[s.hero, { borderColor: color }]}>
|
|
<Text style={s.heroLabel}>보안 점수</Text>
|
|
<Text style={[s.heroScore, { color }]}>{score}</Text>
|
|
<Text style={[s.heroGrade, { color }]}>{score >= 80 ? 'A등급' : score >= 60 ? 'B등급' : 'C등급'}</Text>
|
|
</View>
|
|
|
|
{domains.map((d: any) => {
|
|
const c = d.score >= 80 ? COLORS.success : d.score >= 60 ? COLORS.warning : COLORS.danger
|
|
return (
|
|
<View key={d.name} style={s.row}>
|
|
<Text style={s.dname}>{d.name}</Text>
|
|
<View style={s.bar}><View style={[s.fill, { width: `${d.score}%`, backgroundColor: c }]} /></View>
|
|
<Text style={[s.dscore, { color: c }]}>{d.score}</Text>
|
|
</View>
|
|
)
|
|
})}
|
|
|
|
{data.findings?.length > 0 && (
|
|
<View style={s.findingsCard}>
|
|
<Text style={s.findingsTitle}>주요 발견 사항</Text>
|
|
{data.findings.map((f: string, i: number) => <Text key={i} style={s.finding}>• {f}</Text>)}
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: COLORS.bg },
|
|
empty: { textAlign: 'center', color: COLORS.muted, marginTop: 40 },
|
|
hero: { alignItems: 'center', borderWidth: 2, borderRadius: 16, padding: 24, marginBottom: 16, backgroundColor: '#fff', elevation: 2 },
|
|
heroLabel: { fontSize: 13, color: COLORS.muted },
|
|
heroScore: { fontSize: 64, fontWeight: '900', lineHeight: 72 },
|
|
heroGrade: { fontSize: 16, fontWeight: '700', marginTop: 4 },
|
|
row: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: '#fff', borderRadius: 10, padding: 12, marginBottom: 6, elevation: 1 },
|
|
dname: { fontSize: 12, color: COLORS.text, width: 100 },
|
|
bar: { flex: 1, height: 6, backgroundColor: COLORS.border, borderRadius: 3, overflow: 'hidden' },
|
|
fill: { height: '100%', borderRadius: 3 },
|
|
dscore: { fontSize: 13, fontWeight: '700', width: 28 },
|
|
findingsCard: { backgroundColor: '#fff', borderRadius: 12, padding: 14, marginTop: 8, elevation: 1 },
|
|
findingsTitle:{ fontSize: 14, fontWeight: '700', color: COLORS.danger, marginBottom: 8 },
|
|
finding: { fontSize: 12, color: COLORS.text, marginBottom: 4 },
|
|
})
|