- 37개 파일 IP → zioinfo.co.kr 치환 (소스/매뉴얼/설정/하네스) - Manager DrConsole/NetworkConsole/CsapConsole 빌드 + /var/www/manager/ 배포 - 테스트: Manager HTTP 200, ITSM 신규 API 7개 전체 200 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
5.9 KiB
TypeScript
130 lines
5.9 KiB
TypeScript
import { useState, useRef, useEffect } from 'react'
|
|
import {
|
|
View, Text, TextInput, TouchableOpacity, ScrollView,
|
|
StyleSheet, KeyboardAvoidingView, Platform, ActivityIndicator,
|
|
} from 'react-native'
|
|
import { COLORS } from '../../constants/Config'
|
|
import { sendAIMessage } from '../../services/api'
|
|
import { useAuth } from '../../hooks/useAuth'
|
|
|
|
interface Msg { id: number; role: 'user' | 'ai'; text: string; time: string }
|
|
|
|
const QUICK = ['서버 상태 확인', 'SR 목록 보여줘', '최근 인시던트', '라이선스 현황']
|
|
|
|
export default function ChatScreen() {
|
|
const { user } = useAuth()
|
|
const [msgs, setMsgs] = useState<Msg[]>([
|
|
{ id: 0, role: 'ai', text: `안녕하세요 ${user?.display_name ?? ''}님! 👋\nGUARDiA AI 어시스턴트입니다.\n무엇을 도와드릴까요?`, time: now() },
|
|
])
|
|
const [input, setInput] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const scrollRef = useRef<ScrollView>(null)
|
|
|
|
function now() { return new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }) }
|
|
|
|
const send = async (text = input) => {
|
|
if (!text.trim() || loading) return
|
|
const userMsg: Msg = { id: Date.now(), role: 'user', text: text.trim(), time: now() }
|
|
setMsgs(m => [...m, userMsg])
|
|
setInput('')
|
|
setLoading(true)
|
|
try {
|
|
const r = await sendAIMessage(text.trim())
|
|
const reply = r.data?.reply ?? r.data?.message ?? r.data?.response ?? '응답을 받았습니다.'
|
|
setMsgs(m => [...m, { id: Date.now()+1, role: 'ai', text: reply, time: now() }])
|
|
} catch {
|
|
setMsgs(m => [...m, { id: Date.now()+1, role: 'ai',
|
|
text: '현재 AI 서버에 연결할 수 없습니다. Ollama 서버 상태를 확인해주세요.', time: now() }])
|
|
} finally { setLoading(false) }
|
|
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100)
|
|
}
|
|
|
|
useEffect(() => {
|
|
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: false }), 50)
|
|
}, [msgs])
|
|
|
|
return (
|
|
<KeyboardAvoidingView style={{ flex:1 }} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
|
|
<View style={s.container}>
|
|
{/* 메시지 목록 */}
|
|
<ScrollView ref={scrollRef} style={s.messages} contentContainerStyle={{ padding: 16 }}>
|
|
{msgs.map(m => (
|
|
<View key={m.id} style={[s.msgRow, m.role === 'user' && s.userRow]}>
|
|
{m.role === 'ai' && <Text style={s.avatar}>🤖</Text>}
|
|
<View style={[s.bubble, m.role === 'user' ? s.userBubble : s.aiBubble]}>
|
|
<Text style={[s.bubbleText, m.role === 'user' && s.userText]}>{m.text}</Text>
|
|
<Text style={[s.timeText, m.role === 'user' && { color: 'rgba(255,255,255,.6)' }]}>{m.time}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
{loading && (
|
|
<View style={s.msgRow}>
|
|
<Text style={s.avatar}>🤖</Text>
|
|
<View style={s.aiBubble}>
|
|
<ActivityIndicator size="small" color={COLORS.accent} />
|
|
</View>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{/* 빠른 질문 */}
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}
|
|
style={s.quickScroll} contentContainerStyle={{ paddingHorizontal: 12, gap: 8 }}>
|
|
{QUICK.map(q => (
|
|
<TouchableOpacity key={q} style={s.quickChip} onPress={() => send(q)}>
|
|
<Text style={s.quickChipText}>{q}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
|
|
{/* 입력창 */}
|
|
<View style={s.inputRow}>
|
|
<TextInput
|
|
style={s.textInput}
|
|
value={input}
|
|
onChangeText={setInput}
|
|
placeholder="GUARDiA AI에게 질문하세요..."
|
|
placeholderTextColor={COLORS.muted}
|
|
multiline
|
|
maxLength={500}
|
|
returnKeyType="send"
|
|
onSubmitEditing={() => send()}
|
|
/>
|
|
<TouchableOpacity style={[s.sendBtn, (!input.trim() || loading) && s.sendDisabled]}
|
|
onPress={() => send()} disabled={!input.trim() || loading}>
|
|
<Text style={s.sendIcon}>➤</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex:1, backgroundColor:COLORS.bg },
|
|
messages: { flex:1 },
|
|
msgRow: { flexDirection:'row', alignItems:'flex-end', marginBottom:12, gap:8 },
|
|
userRow: { flexDirection:'row-reverse' },
|
|
avatar: { fontSize:24, marginBottom:4 },
|
|
bubble: { maxWidth:'75%', borderRadius:16, padding:12 },
|
|
aiBubble: { backgroundColor:'#fff', borderBottomLeftRadius:4,
|
|
shadowColor:'#000', shadowOffset:{width:0,height:1}, shadowOpacity:.06, elevation:1 },
|
|
userBubble: { backgroundColor:COLORS.accent, borderBottomRightRadius:4 },
|
|
bubbleText: { fontSize:14, color:COLORS.text, lineHeight:20 },
|
|
userText: { color:'#fff' },
|
|
timeText: { fontSize:10, color:COLORS.muted, marginTop:4, textAlign:'right' },
|
|
quickScroll: { maxHeight:44, borderTopWidth:1, borderTopColor:COLORS.border, backgroundColor:'#fff' },
|
|
quickChip: { alignSelf:'center', backgroundColor:COLORS.light, paddingHorizontal:12,
|
|
paddingVertical:6, borderRadius:20 },
|
|
quickChipText: { fontSize:12, color:COLORS.accent, fontWeight:'500' },
|
|
inputRow: { flexDirection:'row', padding:12, backgroundColor:'#fff',
|
|
borderTopWidth:1, borderTopColor:COLORS.border, alignItems:'flex-end', gap:8 },
|
|
textInput: { flex:1, borderWidth:1.5, borderColor:COLORS.border, borderRadius:20,
|
|
paddingHorizontal:14, paddingVertical:10, fontSize:14, color:COLORS.text,
|
|
maxHeight:100, backgroundColor:'#fafafa' },
|
|
sendBtn: { width:42, height:42, borderRadius:21, backgroundColor:COLORS.accent,
|
|
justifyContent:'center', alignItems:'center' },
|
|
sendDisabled:{ opacity:.4 },
|
|
sendIcon: { color:'#fff', fontSize:16, marginLeft:2 },
|
|
})
|