91 lines
4.0 KiB
TypeScript
91 lines
4.0 KiB
TypeScript
import React, { useState } from 'react'
|
|
import { View, Text, TextInput, FlatList, StyleSheet, TouchableOpacity, ActivityIndicator, Keyboard } from 'react-native'
|
|
import { COLORS } from '../../constants/Config'
|
|
import client from '../../services/api'
|
|
|
|
export default function IOCSearchScreen() {
|
|
const [query, setQuery] = useState('')
|
|
const [results, setResults] = useState<any[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [searched, setSearched] = useState(false)
|
|
|
|
const search = async () => {
|
|
if (!query.trim()) return
|
|
Keyboard.dismiss()
|
|
setLoading(true)
|
|
setSearched(true)
|
|
try {
|
|
const r = await client.get('/api/ai-soc/ioc/search', { params: { q: query.trim() } })
|
|
setResults(r.data?.results ?? r.data?.iocs ?? [])
|
|
} catch { setResults([]) } finally { setLoading(false) }
|
|
}
|
|
|
|
const TYPE_COLOR: Record<string, string> = { ip: COLORS.danger, domain: '#f97316', hash: COLORS.warning, url: COLORS.blue }
|
|
|
|
return (
|
|
<View style={s.container}>
|
|
<View style={s.searchBar}>
|
|
<TextInput
|
|
style={s.input}
|
|
value={query}
|
|
onChangeText={setQuery}
|
|
placeholder="IP, 도메인, 해시, URL 검색..."
|
|
placeholderTextColor={COLORS.muted}
|
|
returnKeyType="search"
|
|
onSubmitEditing={search}
|
|
autoCapitalize="none"
|
|
/>
|
|
<TouchableOpacity style={s.searchBtn} onPress={search}>
|
|
<Text style={s.searchText}>검색</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{loading ? <ActivityIndicator style={{ marginTop: 40 }} color={COLORS.accent} /> : (
|
|
<FlatList
|
|
data={results}
|
|
keyExtractor={(_, i) => String(i)}
|
|
ListEmptyComponent={searched ? <Text style={s.empty}>결과가 없습니다.</Text> : null}
|
|
contentContainerStyle={{ padding: 12 }}
|
|
renderItem={({ item }) => {
|
|
const type = item.type ?? 'unknown'
|
|
const color = TYPE_COLOR[type] ?? COLORS.muted
|
|
return (
|
|
<View style={s.card}>
|
|
<View style={s.row}>
|
|
<View style={[s.typeBadge, { backgroundColor: color + '20' }]}>
|
|
<Text style={[s.typeText, { color }]}>{type.toUpperCase()}</Text>
|
|
</View>
|
|
<Text style={s.value} numberOfLines={1}>{item.value ?? item.ioc}</Text>
|
|
</View>
|
|
<Text style={s.desc} numberOfLines={2}>{item.description ?? item.context ?? ''}</Text>
|
|
<View style={s.metaRow}>
|
|
<Text style={s.meta}>위협: {item.threat_name ?? '-'}</Text>
|
|
<Text style={s.meta}>신뢰도: {item.confidence ?? '-'}%</Text>
|
|
<Text style={s.meta}>{item.first_seen?.slice(0, 10) ?? '-'}</Text>
|
|
</View>
|
|
</View>
|
|
)
|
|
}}
|
|
/>
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: COLORS.bg },
|
|
searchBar: { flexDirection: 'row', gap: 8, padding: 12 },
|
|
input: { flex: 1, backgroundColor: '#fff', borderRadius: 10, paddingHorizontal: 14, paddingVertical: 10, fontSize: 13, color: COLORS.text, elevation: 1 },
|
|
searchBtn: { backgroundColor: COLORS.accent, borderRadius: 10, paddingHorizontal: 16, justifyContent: 'center' },
|
|
searchText: { color: '#fff', fontSize: 13, fontWeight: '700' },
|
|
card: { backgroundColor: '#fff', borderRadius: 10, padding: 14, marginBottom: 8, elevation: 1 },
|
|
row: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 6 },
|
|
typeBadge: { borderRadius: 4, paddingHorizontal: 8, paddingVertical: 3 },
|
|
typeText: { fontSize: 10, fontWeight: '700' },
|
|
value: { flex: 1, fontSize: 13, color: COLORS.text, fontFamily: 'monospace' },
|
|
desc: { fontSize: 12, color: COLORS.muted, marginBottom: 6 },
|
|
metaRow: { flexDirection: 'row', justifyContent: 'space-between' },
|
|
meta: { fontSize: 10, color: COLORS.muted },
|
|
empty: { textAlign: 'center', color: COLORS.muted, marginTop: 40 },
|
|
})
|