110 lines
5.4 KiB
TypeScript
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 },
|
|
});
|