126 lines
4.6 KiB
TypeScript
126 lines
4.6 KiB
TypeScript
/**
|
|
* 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<RCA>(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<RCA>(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 (
|
|
<View style={S.wrap}>
|
|
<ActivityIndicator color={COLORS.accent} />
|
|
<Text style={S.loadingText}>RCA 분석 중...</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<View style={S.wrap}>
|
|
<View style={S.head}>
|
|
<Text style={S.title}>🧩 자동 RCA</Text>
|
|
<Text style={S.badge}>{src === 'api' ? 'ITSM 분석' : src === 'ai' ? 'AI 분석' : '데이터 없음'}</Text>
|
|
</View>
|
|
{sections.map(sec => (
|
|
<View key={sec.key} style={S.accItem}>
|
|
<Pressable style={S.accHead} onPress={() => setOpen(open === sec.key ? null : sec.key)}>
|
|
<Text style={S.accLabel}>
|
|
{sec.icon} {sec.label}
|
|
</Text>
|
|
<Text style={S.accArrow}>{open === sec.key ? '▲' : '▼'}</Text>
|
|
</Pressable>
|
|
{open === sec.key ? (
|
|
<Text style={S.accBody}>{rca[sec.key] || '분석 결과가 없습니다.'}</Text>
|
|
) : null}
|
|
</View>
|
|
))}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
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 },
|
|
})
|