/** * AutoRCA (#21) — 자동 근본원인분석(RCA) 표시 * * GET /api/ai/rca/{incident_id} 우선 시도, 실패 시 Ollama로 직접 분석. * 원인 / 영향 범위 / 재발 방지 3섹션을 접을 수 있는 아코디언으로 표시. */ import { useState, useEffect } from 'react' import { View, Text, Pressable, StyleSheet, ActivityIndicator } from 'react-native' import { COLORS, API_BASE } from '../constants/Config' import { authFetch } from '../utils/auth' import { generateJSON, DEFAULT_TEXT_MODEL } from '../lib/ollama' interface RCA { cause: string impact: string prevention: string } interface Props { incidentId: number | string summary?: string // Ollama 폴백용 인시던트 요약 (자격증명 미포함) } const EMPTY: RCA = { cause: '', impact: '', prevention: '' } export function AutoRCA({ incidentId, summary }: Props) { const [rca, setRca] = useState(EMPTY) const [loading, setLoading] = useState(true) const [open, setOpen] = useState<'cause' | 'impact' | 'prevention' | null>('cause') const [src, setSrc] = useState<'api' | 'ai' | 'none'>('none') useEffect(() => { let alive = true ;(async () => { setLoading(true) // 1) ITSM API 시도 try { const res = await authFetch(`${API_BASE}/api/ai/rca/${incidentId}`) if (res.ok) { const d = await res.json() const parsed: RCA = { cause: d.cause ?? d.root_cause ?? '', impact: d.impact ?? d.impact_scope ?? '', prevention: d.prevention ?? d.recurrence_prevention ?? '', } if (alive && (parsed.cause || parsed.impact || parsed.prevention)) { setRca(parsed) setSrc('api') setLoading(false) return } } } catch { /* API 실패 → Ollama 폴백 */ } // 2) Ollama 폴백 if (summary?.trim()) { const prompt = `다음 IT 인시던트에 대해 근본원인분석(RCA)을 수행하세요: "${summary}". ` + `JSON으로만 출력: {"cause":"근본원인","impact":"영향범위","prevention":"재발방지책"}` const aiRca = await generateJSON(DEFAULT_TEXT_MODEL, prompt, EMPTY) if (alive) { setRca(aiRca) setSrc(aiRca.cause ? 'ai' : 'none') } } if (alive) setLoading(false) })() return () => { alive = false } }, [incidentId, summary]) const sections: { key: 'cause' | 'impact' | 'prevention'; label: string; icon: string }[] = [ { key: 'cause', label: '근본 원인', icon: '🔍' }, { key: 'impact', label: '영향 범위', icon: '🌐' }, { key: 'prevention', label: '재발 방지', icon: '🛡️' }, ] if (loading) { return ( RCA 분석 중... ) } return ( 🧩 자동 RCA {src === 'api' ? 'ITSM 분석' : src === 'ai' ? 'AI 분석' : '데이터 없음'} {sections.map(sec => ( setOpen(open === sec.key ? null : sec.key)}> {sec.icon} {sec.label} {open === sec.key ? '▲' : '▼'} {open === sec.key ? ( {rca[sec.key] || '분석 결과가 없습니다.'} ) : null} ))} ) } export default AutoRCA const S = StyleSheet.create({ wrap: { backgroundColor: COLORS.card, borderRadius: 14, padding: 14, borderWidth: 1, borderColor: COLORS.border }, head: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, title: { fontSize: 15, fontWeight: '700', color: COLORS.text }, badge: { fontSize: 10, color: COLORS.blue, backgroundColor: COLORS.light, paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8, overflow: 'hidden' }, loadingText: { fontSize: 12, color: COLORS.muted, marginTop: 6 }, accItem: { borderTopWidth: 1, borderTopColor: COLORS.border }, accHead: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 11 }, accLabel: { fontSize: 13, fontWeight: '600', color: COLORS.text }, accArrow: { fontSize: 10, color: COLORS.muted }, accBody: { fontSize: 13, color: COLORS.text, lineHeight: 19, paddingBottom: 12, paddingLeft: 4 }, })