import { useEffect, useState, useCallback } from 'react' import { View, Text, ScrollView, TouchableOpacity, StyleSheet, RefreshControl, Alert, ActivityIndicator, TextInput, } from 'react-native' import { COLORS } from '../../constants/Config' import { getNetworkDevices, backupNetworkDevice } from '../../services/api' interface NetworkDevice { id: number; device_name: string; device_type: string vendor: string; model?: string; location?: string is_active: boolean; last_backup_at?: string | null } const DEVICE_ICON: Record = { SWITCH: 'πŸ”€', ROUTER: 'πŸ”—', FIREWALL: 'πŸ›‘οΈ', LOAD_BALANCER: 'βš–οΈ', } const VENDOR_COLOR: Record = { CISCO: '#1ba0d7', HUAWEI: '#cf0a2c', JUNIPER: '#84bd00', PIOLINK: '#003087', } function daysSince(iso?: string | null): number | null { if (!iso) return null return Math.floor((Date.now() - new Date(iso).getTime()) / 86400000) } export default function NetworkScreen() { const [devices, setDevices] = useState([]) const [loading, setLoading] = useState(true) const [refresh, setRefresh] = useState(false) const [backing, setBacking] = useState(null) const [search, setSearch] = useState('') const load = useCallback(async (isRefresh = false) => { isRefresh ? setRefresh(true) : setLoading(true) try { const r = await getNetworkDevices() setDevices(r.data) } catch { /* λ¬΄μ‹œ */ } finally { setLoading(false); setRefresh(false) } }, []) useEffect(() => { load() }, [load]) const handleBackup = (id: number, name: string) => { Alert.alert('μ„€μ • λ°±μ—…', `"${name}" 섀정을 λ°±μ—…ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?`, [ { text: 'μ·¨μ†Œ', style: 'cancel' }, { text: 'λ°±μ—…', onPress: async () => { setBacking(id) try { const r = await backupNetworkDevice(id) const changed = r.data.changed_lines > 0 ? ` (λ³€κ²½ ${r.data.changed_lines}쀄 감지!)` : '' Alert.alert('μ™„λ£Œ', `λ°±μ—… 성곡${changed}`) load() } catch (e: any) { Alert.alert('였λ₯˜', e.response?.data?.detail ?? 'λ°±μ—… μ‹€νŒ¨') } finally { setBacking(null) } }}, ]) } const filtered = devices.filter(d => !search || [d.device_name, d.vendor, d.device_type, d.location ?? ''] .some(v => v.toLowerCase().includes(search.toLowerCase())) ) const noBackup = devices.filter(d => !d.last_backup_at).length const stale = devices.filter(d => (daysSince(d.last_backup_at) ?? 999) > 7).length if (loading) return ( ) return ( load(true)} tintColor={COLORS.accent} />}> {/* μš”μ•½ */} {[ { label: '전체', value: devices.length, color: COLORS.text }, { label: 'λ―Έλ°±μ—…', value: noBackup, color: '#ef4444' }, { label: '7일초과', value: stale, color: '#f59e0b' }, { label: '정상', value: devices.length - noBackup - stale, color: '#22c55e' }, ].map(c => ( {c.value} {c.label} ))} {/* 검색 */} {/* μž₯λΉ„ λͺ©λ‘ */} λ„€νŠΈμ›Œν¬ μž₯λΉ„ ({filtered.length}개) {filtered.length === 0 ? {devices.length === 0 ? 'λ“±λ‘λœ μž₯λΉ„κ°€ μ—†μŠ΅λ‹ˆλ‹€.' : '검색 κ²°κ³Ό μ—†μŒ'} : filtered.map(d => { const days = daysSince(d.last_backup_at) const backupOk = days !== null && days <= 7 return ( {DEVICE_ICON[d.device_type] ?? 'πŸ”§'} {d.device_name} {d.vendor} {d.model ? ` Β· ${d.model}` : ''} {d.location ? ` πŸ“${d.location}` : ''} {!d.last_backup_at ? '⚠ λ―Έλ°±μ—…' : days! > 7 ? `⚠ ${days}일 μ „ λ°±μ—… (κ°±μ‹  ν•„μš”)` : `βœ” ${days}일 μ „ λ°±μ—…`} handleBackup(d.id, d.device_name)} disabled={backing === d.id}> {backing === d.id ? 'λ°±μ—… 쀑...' : 'λ°±μ—…'} ) })} ) } const s = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5f7fa' }, center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, row: { flexDirection: 'row', gap: 10, padding: 16 }, statCard: { flex: 1, backgroundColor: '#fff', borderRadius: 10, padding: 12, alignItems: 'center', shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 4, elevation: 2 }, statNum: { fontSize: 22, fontWeight: '700' }, statLabel: { fontSize: 10, color: '#94a3b8', marginTop: 3 }, searchWrap: { paddingHorizontal: 16, marginBottom: 8 }, searchInput: { backgroundColor: '#fff', borderRadius: 8, padding: 10, fontSize: 13, color: COLORS.text, borderWidth: 1, borderColor: '#e2e8f0' }, sectionTitle: { fontSize: 13, fontWeight: '700', color: '#1e293b', paddingHorizontal: 16, marginBottom: 8 }, card: { backgroundColor: '#fff', borderRadius: 10, padding: 14, marginHorizontal: 16, marginBottom: 10, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 4, elevation: 2 }, cardTop: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 10 }, icon: { fontSize: 24 }, deviceName: { fontSize: 14, fontWeight: '600', color: '#1e293b' }, vendorText: { fontSize: 12, color: '#64748b', marginTop: 2 }, statusDot: { width: 10, height: 10, borderRadius: 5 }, backupRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, backupText: { fontSize: 12, color: '#64748b', flex: 1 }, backupBtn: { backgroundColor: COLORS.accent, borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6 }, backupBtnDisabled: { backgroundColor: '#e2e8f0' }, backupBtnText: { color: '#fff', fontSize: 12, fontWeight: '600' }, empty: { color: '#94a3b8', textAlign: 'center', padding: 30, fontSize: 13 }, })