/** * #33 등록 디바이스 관리 * GET /api/auth/devices — 디바이스 목록 * DELETE /api/auth/devices/{id} — 등록 해제 * 현재 기기는 삭제 버튼 비활성화 */ import { useCallback, useEffect, useState } from 'react' import { View, Text, FlatList, TouchableOpacity, StyleSheet, RefreshControl, Alert, ActivityIndicator, Platform, } from 'react-native' import { COLORS } from '../../constants/Config' import { getDevices, deleteDevice } from '../../services/api' import LineIcon from '../../components/LineIcon' interface Device { id: string | number name?: string device_name?: string os?: string platform?: string last_seen?: string last_active_at?: string is_current?: boolean current?: boolean } function osIcon(os?: string): Parameters[0]['name'] { const v = (os ?? '').toLowerCase() if (v.includes('ios') || v.includes('iphone') || v.includes('mac')) return 'lock' if (v.includes('android')) return 'server' return 'dashboard' } function fmt(d?: string): string { if (!d) return '-' try { const dt = new Date(d) if (isNaN(dt.getTime())) return d return dt.toLocaleString('ko-KR', { dateStyle: 'medium', timeStyle: 'short' }) } catch { return d } } export default function DevicesScreen() { const [devices, setDevices] = useState([]) const [loading, setLoading] = useState(true) const [refresh, setRefresh] = useState(false) const load = useCallback(async (isRefresh = false) => { isRefresh ? setRefresh(true) : setLoading(true) try { const r = await getDevices() const list: Device[] = Array.isArray(r.data) ? r.data : r.data?.items ?? [] setDevices(list) } catch { setDevices([]) } finally { setLoading(false) setRefresh(false) } }, []) useEffect(() => { load() }, [load]) const handleRemove = (dev: Device) => { const name = dev.name ?? dev.device_name ?? '이 기기' Alert.alert('디바이스 등록 해제', `"${name}"의 등록을 해제하시겠습니까?\n해당 기기는 다시 로그인해야 합니다.`, [ { text: '취소', style: 'cancel' }, { text: '해제', style: 'destructive', onPress: async () => { try { await deleteDevice(dev.id) setDevices((prev) => prev.filter((d) => d.id !== dev.id)) } catch (e: any) { Alert.alert('오류', e.response?.data?.detail ?? '등록 해제에 실패했습니다.') } }, }, ]) } if (loading) { return ( ) } return ( String(d.id)} refreshControl={ load(true)} tintColor={COLORS.accent} />} ListHeaderComponent={ 등록된 디바이스 이 계정으로 로그인된 기기 {devices.length}대 } ListEmptyComponent={ 등록된 디바이스가 없습니다. } contentContainerStyle={devices.length === 0 ? { flexGrow: 1 } : undefined} renderItem={({ item }) => { const isCurrent = item.is_current ?? item.current ?? false return ( {item.name ?? item.device_name ?? '알 수 없는 기기'} {isCurrent && ( 현재 기기 )} {item.os ?? item.platform ?? Platform.OS} 마지막 접속: {fmt(item.last_seen ?? item.last_active_at)} handleRemove(item)} > {isCurrent ? '사용 중' : '해제'} ) }} /> ) } const s = StyleSheet.create({ center: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: COLORS.bg }, header: { padding: 20, paddingBottom: 8 }, headerTitle: { fontSize: 18, fontWeight: '800', color: COLORS.text }, headerSub: { fontSize: 13, color: COLORS.muted, marginTop: 4 }, card: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#fff', marginHorizontal: 16, marginTop: 10, borderRadius: 14, padding: 16, borderWidth: 1, borderColor: COLORS.border, }, iconBox: { width: 42, height: 42, borderRadius: 11, backgroundColor: 'rgba(0,160,200,.08)', alignItems: 'center', justifyContent: 'center', }, row: { flexDirection: 'row', alignItems: 'center', gap: 8 }, name: { fontSize: 15, fontWeight: '700', color: COLORS.text }, currentBadge: { backgroundColor: '#dcfce7', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 8 }, currentText: { fontSize: 10, fontWeight: '700', color: '#15803d' }, meta: { fontSize: 12, color: COLORS.muted, marginTop: 2 }, removeBtn: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10, backgroundColor: '#fee2e2', }, removeDisabled: { backgroundColor: '#f1f5f9' }, removeText: { fontSize: 13, fontWeight: '700', color: COLORS.danger }, removeTextDisabled: { color: COLORS.muted }, empty: { flex: 1, alignItems: 'center', justifyContent: 'center' }, emptyText: { color: COLORS.muted, fontSize: 14 }, })