/** * ImpactPreview (#27) — 장애 영향 서비스 예측 시각화 * * GET /api/ai/impact/{server_id} 우선, 실패 시 /api/servers/{id}/dependencies. * 영향받는 서비스 목록 + 연결 관계를 텍스트 기반 리스트로 표시. */ import { useState, useEffect } from 'react' import { View, Text, StyleSheet, ActivityIndicator } from 'react-native' import { COLORS, API_BASE } from '../constants/Config' import { authFetch } from '../utils/auth' interface ImpactNode { name: string type?: string // service / db / app ... severity?: 'high' | 'medium' | 'low' | string relation?: string // 'depends_on' 등 } interface Props { serverId: number | string serverName?: string } const SEV_COLOR: Record = { high: COLORS.danger, medium: COLORS.warning, low: COLORS.success, } export function ImpactPreview({ serverId, serverName }: Props) { const [nodes, setNodes] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { let alive = true ;(async () => { setLoading(true) const endpoints = [ `${API_BASE}/api/ai/impact/${serverId}`, `${API_BASE}/api/servers/${serverId}/dependencies`, ] for (const url of endpoints) { try { const res = await authFetch(url) if (res.ok) { const d = await res.json() const raw = Array.isArray(d) ? d : d.impacted ?? d.services ?? d.dependencies ?? d.nodes ?? [] const list: ImpactNode[] = raw.map((x: any) => ({ name: x.name ?? x.service_name ?? x.hostname ?? String(x), type: x.type ?? x.kind, severity: x.severity ?? x.impact, relation: x.relation ?? x.relationship, })) if (alive && list.length) { setNodes(list) setLoading(false) return } } } catch { /* 다음 엔드포인트 시도 */ } } if (alive) { setNodes([]) setLoading(false) } })() return () => { alive = false } }, [serverId]) return ( 🌐 장애 영향도 예측 {serverName ? 출발: {serverName} : null} {loading ? ( ) : nodes.length === 0 ? ( 영향 데이터를 불러올 수 없습니다. ) : ( {nodes.map((n, i) => ( {n.name} {n.type ? `${n.type} · ` : ''} {n.relation ?? '연결됨'} {n.severity ? ` · ${n.severity}` : ''} ))} ※ {nodes.length}개 서비스가 영향을 받을 수 있습니다. )} ) } export default ImpactPreview const S = StyleSheet.create({ wrap: { backgroundColor: COLORS.card, borderRadius: 14, padding: 14, borderWidth: 1, borderColor: COLORS.border }, title: { fontSize: 15, fontWeight: '700', color: COLORS.text }, root: { fontSize: 12, color: COLORS.muted, marginTop: 2 }, empty: { fontSize: 12, color: COLORS.muted, marginTop: 8 }, list: { marginTop: 10 }, node: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', gap: 10 }, dot: { width: 10, height: 10, borderRadius: 5 }, nodeName: { fontSize: 13, fontWeight: '600', color: COLORS.text }, nodeMeta: { fontSize: 11, color: COLORS.muted, marginTop: 2 }, note: { fontSize: 11, color: COLORS.danger, marginTop: 8 }, })