78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import React, { useState, useCallback } from 'react'
|
|
import { View, Text, FlatList, StyleSheet, RefreshControl, ActivityIndicator } from 'react-native'
|
|
import { COLORS } from '../../constants/Config'
|
|
import { getAuditLogs } from '../../services/api'
|
|
|
|
function maskIp(ip: string | undefined) {
|
|
if (!ip) return '-'
|
|
const parts = ip.split('.')
|
|
if (parts.length === 4) return `${parts[0]}.xxx.xxx.xxx`
|
|
return ip.replace(/[0-9]+/g, 'x')
|
|
}
|
|
|
|
export default function AuditLogScreen() {
|
|
const [items, setItems] = useState<any[]>([])
|
|
const [page, setPage] = useState(0)
|
|
const [loading, setLoading] = useState(false)
|
|
const [loadingMore, setLoadingMore] = useState(false)
|
|
const [hasMore, setHasMore] = useState(true)
|
|
|
|
const load = useCallback(async (pg = 0) => {
|
|
if (pg === 0) setLoading(true)
|
|
else setLoadingMore(true)
|
|
try {
|
|
const r = await getAuditLogs(pg)
|
|
const data = r.data?.items ?? r.data ?? []
|
|
if (pg === 0) setItems(data)
|
|
else setItems(prev => [...prev, ...data])
|
|
setHasMore(data.length >= 30)
|
|
setPage(pg)
|
|
} catch {}
|
|
finally { setLoading(false); setLoadingMore(false) }
|
|
}, [])
|
|
|
|
React.useEffect(() => { load(0) }, [])
|
|
|
|
const loadMore = () => { if (!loadingMore && hasMore) load(page + 1) }
|
|
|
|
const renderItem = ({ item }: { item: any }) => (
|
|
<View style={s.card}>
|
|
<View style={s.row}>
|
|
<Text style={s.actor}>{item.actor}</Text>
|
|
<Text style={s.time}>{item.created_at?.slice(0, 16).replace('T', ' ')}</Text>
|
|
</View>
|
|
<Text style={s.action}>{item.action}</Text>
|
|
<Text style={s.detail} numberOfLines={2}>{item.detail}</Text>
|
|
<Text style={s.ip}>IP: {maskIp(item.ip_hash ?? item.ip_addr)}</Text>
|
|
</View>
|
|
)
|
|
|
|
if (loading) return <ActivityIndicator style={{ flex: 1 }} color={COLORS.accent} />
|
|
|
|
return (
|
|
<FlatList
|
|
data={items}
|
|
keyExtractor={(_, i) => String(i)}
|
|
renderItem={renderItem}
|
|
refreshControl={<RefreshControl refreshing={loading} onRefresh={() => load(0)} />}
|
|
onEndReached={loadMore}
|
|
onEndReachedThreshold={0.3}
|
|
ListFooterComponent={loadingMore ? <ActivityIndicator color={COLORS.accent} style={{ padding: 16 }} /> : null}
|
|
ListEmptyComponent={<Text style={s.empty}>감사 로그가 없습니다.</Text>}
|
|
contentContainerStyle={{ padding: 12 }}
|
|
style={{ backgroundColor: COLORS.bg }}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
card: { backgroundColor: '#fff', borderRadius: 8, padding: 12, marginBottom: 6, elevation: 1 },
|
|
row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
|
|
actor: { fontSize: 13, fontWeight: '700', color: COLORS.text },
|
|
time: { fontSize: 11, color: COLORS.muted },
|
|
action: { fontSize: 12, color: COLORS.accent, fontWeight: '600', marginBottom: 2 },
|
|
detail: { fontSize: 12, color: COLORS.text },
|
|
ip: { fontSize: 11, color: COLORS.muted, marginTop: 4 },
|
|
empty: { textAlign: 'center', color: COLORS.muted, marginTop: 40 },
|
|
})
|