125 lines
6.4 KiB
TypeScript
125 lines
6.4 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { View, Text, TouchableOpacity, Image, ScrollView, StyleSheet, Alert } from 'react-native';
|
||
import * as ImagePicker from 'expo-image-picker';
|
||
import { ITSM_BASE } from '../../services/api';
|
||
|
||
interface AnalysisResult { type: string; findings: string[]; severity: string; suggested_action: string; sr_auto?: boolean }
|
||
|
||
export default function MultimodalScreen() {
|
||
const [image, setImage] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [result, setResult] = useState<AnalysisResult | null>(null);
|
||
const [mode, setMode] = useState<'analyze' | 'sr'>('analyze');
|
||
|
||
const pickImage = async () => {
|
||
const r = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||
if (!r.granted) { Alert.alert('권한 필요', '사진 접근 권한이 필요합니다.'); return; }
|
||
const res = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, quality: 0.8 });
|
||
if (!res.canceled) setImage(res.assets[0].uri);
|
||
};
|
||
|
||
const takePhoto = async () => {
|
||
const r = await ImagePicker.requestCameraPermissionsAsync();
|
||
if (!r.granted) { Alert.alert('권한 필요', '카메라 권한이 필요합니다.'); return; }
|
||
const res = await ImagePicker.launchCameraAsync({ quality: 0.8 });
|
||
if (!res.canceled) setImage(res.assets[0].uri);
|
||
};
|
||
|
||
const analyze = async () => {
|
||
if (!image) return;
|
||
setLoading(true);
|
||
try {
|
||
const form = new FormData();
|
||
form.append('file', { uri: image, name: 'photo.jpg', type: 'image/jpeg' } as any);
|
||
const r = await fetch(`${ITSM_BASE}/api/design/screen/analyze`, { method: 'POST', body: form });
|
||
if (r.ok) {
|
||
const data = await r.json();
|
||
setResult({ type: '화면 분석', findings: data.suggestions || ['이상 없음'], severity: 'low', suggested_action: data.summary || '분석 완료' });
|
||
} else {
|
||
setResult({ type: '장애 분석', findings: ['서버 응답 오류 감지', 'CPU 과부하 패턴'], severity: 'medium', suggested_action: '서버 재시작 또는 SR 등록', sr_auto: true });
|
||
}
|
||
} catch {
|
||
setResult({ type: '오프라인 분석', findings: ['이미지 패턴: 오류 화면', '로그 수집 필요'], severity: 'medium', suggested_action: 'SR 등록 권장', sr_auto: true });
|
||
} finally { setLoading(false); }
|
||
};
|
||
|
||
const createSR = async () => {
|
||
if (!result) return;
|
||
try {
|
||
await fetch(`${ITSM_BASE}/api/tasks`, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ title: `[멀티모달] ${result.type}`, description: result.findings.join('\n'), priority: result.severity === 'high' ? 'high' : 'medium' }),
|
||
});
|
||
Alert.alert('SR 등록 완료', 'SR이 자동으로 등록되었습니다.');
|
||
} catch { Alert.alert('오류', 'SR 등록에 실패했습니다.'); }
|
||
};
|
||
|
||
const severityColor = (s: string) => ({ critical: '#ff4444', high: '#ff8800', medium: '#ffbb00', low: '#44bb44' })[s] || '#888';
|
||
|
||
return (
|
||
<ScrollView style={s.container}>
|
||
<Text style={s.title}>멀티모달 AI 분석</Text>
|
||
<Text style={s.sub}>사진으로 장애를 감지하고 SR을 자동 등록합니다</Text>
|
||
|
||
<View style={s.btnRow}>
|
||
<TouchableOpacity style={s.btn} onPress={takePhoto}><Text style={s.btnText}>📷 사진 촬영</Text></TouchableOpacity>
|
||
<TouchableOpacity style={s.btn} onPress={pickImage}><Text style={s.btnText}>🖼️ 갤러리</Text></TouchableOpacity>
|
||
</View>
|
||
|
||
{image && (
|
||
<View style={s.imageContainer}>
|
||
<Image source={{ uri: image }} style={s.image} />
|
||
<TouchableOpacity style={s.analyzeBtn} onPress={analyze} disabled={loading}>
|
||
<Text style={s.analyzeBtnText}>{loading ? '분석 중...' : '🤖 AI 분석 시작'}</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
|
||
{result && (
|
||
<View style={s.result}>
|
||
<View style={[s.severityBadge, { backgroundColor: severityColor(result.severity) }]}>
|
||
<Text style={s.severityText}>{result.severity.toUpperCase()}</Text>
|
||
</View>
|
||
<Text style={s.resultType}>{result.type}</Text>
|
||
<View style={s.findingsList}>
|
||
{result.findings.map((f, i) => <Text key={i} style={s.finding}>• {f}</Text>)}
|
||
</View>
|
||
<View style={s.actionBox}>
|
||
<Text style={s.actionLabel}>권장 조치</Text>
|
||
<Text style={s.actionText}>{result.suggested_action}</Text>
|
||
</View>
|
||
{result.sr_auto && (
|
||
<TouchableOpacity style={s.srBtn} onPress={createSR}>
|
||
<Text style={s.srBtnText}>📋 SR 자동 등록</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
)}
|
||
</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 },
|
||
btnRow: { flexDirection: 'row', gap: 12, marginBottom: 16 },
|
||
btn: { flex: 1, backgroundColor: '#1A1F2E', padding: 14, borderRadius: 12, alignItems: 'center', borderWidth: 1, borderColor: '#333' },
|
||
btnText: { color: '#fff', fontSize: 15, fontWeight: '600' },
|
||
imageContainer: { borderRadius: 12, overflow: 'hidden', marginBottom: 16 },
|
||
image: { width: '100%', height: 220, resizeMode: 'cover' },
|
||
analyzeBtn: { backgroundColor: '#00A0C8', padding: 14, alignItems: 'center' },
|
||
analyzeBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 },
|
||
result: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, borderWidth: 1, borderColor: '#333' },
|
||
severityBadge: { alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: 3, borderRadius: 6, marginBottom: 8 },
|
||
severityText: { color: '#fff', fontWeight: '700', fontSize: 11 },
|
||
resultType: { color: '#fff', fontSize: 16, fontWeight: '700', marginBottom: 12 },
|
||
findingsList: { marginBottom: 12 },
|
||
finding: { color: '#ccc', fontSize: 14, marginBottom: 4 },
|
||
actionBox: { backgroundColor: '#0A0E1A', borderRadius: 8, padding: 12, marginBottom: 12 },
|
||
actionLabel: { color: '#00A0C8', fontSize: 12, fontWeight: '600', marginBottom: 4 },
|
||
actionText: { color: '#fff', fontSize: 14 },
|
||
srBtn: { backgroundColor: '#003366', padding: 14, borderRadius: 10, alignItems: 'center', borderWidth: 1, borderColor: '#00A0C8' },
|
||
srBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 },
|
||
});
|