115 lines
6.0 KiB
TypeScript
115 lines
6.0 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
|
|
import { ITSM_BASE } from '../../services/api';
|
|
|
|
interface Decision { id: string; question: string; recommendation: string; confidence: number; alternatives: string[]; impact: string; ts: string }
|
|
|
|
const MOCK_DECISIONS: Decision[] = [
|
|
{ id: 'D-001', question: 'db-01 디스크 85% — 즉시 조치 필요?', recommendation: '로그 파일 정리 + SR 등록', confidence: 0.91, alternatives: ['서버 증설 요청', '아카이브 이전', '임시 파일 삭제'], impact: '서비스 중단 없이 2시간 내 해결 가능', ts: new Date().toISOString() },
|
|
{ id: 'D-002', question: '토요일 오전 2시 긴급 패치 배포 승인?', recommendation: '승인 — 위험 낮음, 영향 범위 최소', confidence: 0.78, alternatives: ['일정 연기 (다음 주)', '부분 배포 (1개 서버만)'], impact: '서비스 다운타임 예상 15분', ts: new Date().toISOString() },
|
|
];
|
|
|
|
export default function AIDecisionScreen() {
|
|
const [decisions, setDecisions] = useState<Decision[]>(MOCK_DECISIONS);
|
|
const [loading, setLoading] = useState(false);
|
|
const [selected, setSelected] = useState<Decision | null>(null);
|
|
|
|
const fetchDecisions = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const r = await fetch(`${ITSM_BASE}/api/ai/decisions/pending`);
|
|
if (r.ok) { const d = await r.json(); if (d.items?.length) setDecisions(d.items); }
|
|
} catch {}
|
|
setLoading(false);
|
|
};
|
|
|
|
useEffect(() => { fetchDecisions(); }, []);
|
|
|
|
const applyDecision = async (dec: Decision, choice: string) => {
|
|
try {
|
|
await fetch(`${ITSM_BASE}/api/ai/decisions/${dec.id}/apply`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ choice }),
|
|
});
|
|
setDecisions(prev => prev.filter(d => d.id !== dec.id));
|
|
setSelected(null);
|
|
} catch {}
|
|
};
|
|
|
|
if (selected) {
|
|
return (
|
|
<ScrollView style={s.container}>
|
|
<TouchableOpacity style={s.backBtn} onPress={() => setSelected(null)}>
|
|
<Text style={s.backText}>← 목록으로</Text>
|
|
</TouchableOpacity>
|
|
<Text style={s.detailTitle}>{selected.question}</Text>
|
|
<View style={s.confCard}>
|
|
<Text style={s.confLabel}>AI 신뢰도</Text>
|
|
<Text style={s.confVal}>{Math.round(selected.confidence * 100)}%</Text>
|
|
</View>
|
|
<View style={s.card}>
|
|
<Text style={s.sectionLbl}>AI 권고</Text>
|
|
<Text style={s.recommendation}>{selected.recommendation}</Text>
|
|
</View>
|
|
<View style={s.card}>
|
|
<Text style={s.sectionLbl}>예상 영향</Text>
|
|
<Text style={s.impact}>{selected.impact}</Text>
|
|
</View>
|
|
<Text style={s.sectionLbl}>선택지</Text>
|
|
{[selected.recommendation, ...selected.alternatives].map((alt, i) => (
|
|
<TouchableOpacity key={i} style={[s.altBtn, i === 0 && s.altBtnPrimary]} onPress={() => applyDecision(selected, alt)}>
|
|
<Text style={[s.altBtnText, i === 0 && s.altBtnTextPrimary]}>{i === 0 ? '✅ ' : ''}{alt}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={s.container}>
|
|
<Text style={s.title}>AI 의사결정 지원</Text>
|
|
<Text style={s.sub}>GUARDiA AI가 운영 결정을 도와드립니다</Text>
|
|
{loading && <ActivityIndicator color="#00A0C8" />}
|
|
{decisions.map(dec => (
|
|
<TouchableOpacity key={dec.id} style={s.decCard} onPress={() => setSelected(dec)}>
|
|
<View style={s.decHeader}>
|
|
<View style={[s.confBadge, { backgroundColor: dec.confidence >= 0.85 ? '#44bb44' : '#ffbb00' }]}>
|
|
<Text style={s.confBadgeText}>{Math.round(dec.confidence * 100)}%</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={s.question}>{dec.question}</Text>
|
|
<Text style={s.recoPreview}>{dec.recommendation}</Text>
|
|
<Text style={s.ts}>{new Date(dec.ts).toLocaleString()}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
{decisions.length === 0 && <Text style={s.empty}>대기 중인 AI 의사결정 없음</Text>}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#0A0E1A', padding: 16 },
|
|
title: { color: '#fff', fontSize: 20, fontWeight: '700', marginBottom: 4 },
|
|
sub: { color: '#888', fontSize: 13, marginBottom: 16 },
|
|
backBtn: { marginBottom: 16 }, backText: { color: '#00A0C8', fontSize: 15 },
|
|
detailTitle: { color: '#fff', fontSize: 17, fontWeight: '700', marginBottom: 16 },
|
|
confCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1, borderColor: '#333' },
|
|
confLabel: { color: '#aaa', fontSize: 14 }, confVal: { color: '#44bb44', fontSize: 28, fontWeight: '700' },
|
|
card: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: '#333' },
|
|
sectionLbl: { color: '#888', fontSize: 12, marginBottom: 8, fontWeight: '600' },
|
|
recommendation: { color: '#fff', fontSize: 15, fontWeight: '600' },
|
|
impact: { color: '#aaa', fontSize: 14 },
|
|
altBtn: { backgroundColor: '#1A1F2E', borderRadius: 10, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#333' },
|
|
altBtnPrimary: { backgroundColor: '#003366', borderColor: '#00A0C8' },
|
|
altBtnText: { color: '#aaa', fontSize: 14 },
|
|
altBtnTextPrimary: { color: '#fff', fontWeight: '700' },
|
|
decCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#333' },
|
|
decHeader: { flexDirection: 'row', marginBottom: 8 },
|
|
confBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 },
|
|
confBadgeText: { color: '#fff', fontSize: 11, fontWeight: '700' },
|
|
question: { color: '#fff', fontWeight: '600', fontSize: 14, marginBottom: 6 },
|
|
recoPreview: { color: '#00A0C8', fontSize: 12, marginBottom: 6 },
|
|
ts: { color: '#555', fontSize: 11 },
|
|
empty: { color: '#555', textAlign: 'center', marginTop: 40, fontSize: 14 },
|
|
});
|