guardia-messenger/app/(tabs)/smart_search.tsx

110 lines
5.4 KiB
TypeScript

import React, { useState, useRef, useCallback } from 'react';
import { View, Text, ScrollView, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
import { ITSM_BASE } from '../../services/api';
type ResultType = 'sr' | 'server' | 'kb' | 'log' | 'user';
interface SearchResult { id: string; type: ResultType; title: string; summary: string; score: number; ts?: string }
const TYPE_ICON: Record<ResultType, string> = { sr: '📋', server: '🖥', kb: '📚', log: '📝', user: '👤' };
export default function SmartSearchScreen() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [recent, setRecent] = useState(['CPU 과부하', 'db-01 디스크', 'nginx 재시작']);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const search = useCallback(async (q: string) => {
if (!q.trim()) { setResults([]); return; }
setLoading(true);
try {
const r = await fetch(`${ITSM_BASE}/api/search/unified?q=${encodeURIComponent(q)}&limit=20`);
if (r.ok) {
const d = await r.json();
setResults(d.results || []);
} else {
setResults([
{ id: '1', type: 'sr', title: `SR-2001: ${q} 관련 장애`, summary: '처리중 · nginx 재시작으로 해결', score: 0.94, ts: '10분 전' },
{ id: '2', type: 'kb', title: `KB: ${q} 대처 방법`, summary: '지식베이스 문서 3건 검색됨', score: 0.87 },
{ id: '3', type: 'server', title: `app-01 · ${q}`, summary: 'CPU 42% · RAM 67% · 정상', score: 0.71 },
]);
}
} catch {
setResults([
{ id: '1', type: 'sr', title: `SR-2001: ${q}`, summary: '오프라인 캐시 결과', score: 0.8, ts: '캐시' },
]);
}
setLoading(false);
if (!recent.includes(q)) setRecent(prev => [q, ...prev].slice(0, 5));
}, [recent]);
const handleChange = (text: string) => {
setQuery(text);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => search(text), 400);
};
const typeColor = (t: ResultType) => ({ sr: '#00A0C8', server: '#ff8800', kb: '#44bb44', log: '#888', user: '#bb44bb' })[t];
return (
<View style={s.container}>
<Text style={s.title}> </Text>
<View style={s.searchRow}>
<TextInput style={s.input} value={query} onChangeText={handleChange} placeholder="SR·서버·KB·로그 통합 검색..." placeholderTextColor="#555" returnKeyType="search" onSubmitEditing={() => search(query)} />
{loading && <ActivityIndicator color="#00A0C8" size="small" style={s.loader} />}
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{!query && (
<View>
<Text style={s.sectionTitle}> </Text>
{recent.map((r, i) => (
<TouchableOpacity key={i} style={s.recentRow} onPress={() => { setQuery(r); search(r); }}>
<Text style={s.recentText}>🕐 {r}</Text>
</TouchableOpacity>
))}
</View>
)}
{results.map(result => (
<View key={result.id} style={s.resultCard}>
<View style={s.resultHeader}>
<Text style={s.typeIcon}>{TYPE_ICON[result.type]}</Text>
<View style={[s.typeBadge, { backgroundColor: typeColor(result.type) + '33', borderColor: typeColor(result.type) }]}>
<Text style={[s.typeBadgeText, { color: typeColor(result.type) }]}>{result.type.toUpperCase()}</Text>
</View>
<Text style={s.scoreText}>{Math.round(result.score * 100)}%</Text>
</View>
<Text style={s.resultTitle}>{result.title}</Text>
<Text style={s.resultSummary}>{result.summary}</Text>
{result.ts && <Text style={s.resultTs}>{result.ts}</Text>}
</View>
))}
{query && !loading && results.length === 0 && (
<Text style={s.empty}>"{query}" </Text>
)}
</ScrollView>
</View>
);
}
const s = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0A0E1A', padding: 16 },
title: { color: '#fff', fontSize: 20, fontWeight: '700', marginBottom: 12 },
searchRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#1A1F2E', borderRadius: 12, marginBottom: 16, borderWidth: 1, borderColor: '#333', paddingRight: 12 },
input: { flex: 1, color: '#fff', fontSize: 15, padding: 14 },
loader: { marginLeft: 8 },
sectionTitle: { color: '#888', fontSize: 13, fontWeight: '600', marginBottom: 8 },
recentRow: { paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#1A1F2E' },
recentText: { color: '#aaa', fontSize: 14 },
resultCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#333' },
resultHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, gap: 8 },
typeIcon: { fontSize: 16 },
typeBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, borderWidth: 1 },
typeBadgeText: { fontSize: 11, fontWeight: '700' },
scoreText: { color: '#888', fontSize: 12, marginLeft: 'auto' },
resultTitle: { color: '#fff', fontWeight: '600', fontSize: 14, marginBottom: 4 },
resultSummary: { color: '#aaa', fontSize: 12, marginBottom: 4 },
resultTs: { color: '#555', fontSize: 11 },
empty: { color: '#555', textAlign: 'center', marginTop: 40 },
});