guardia-messenger/app/(tabs)/chat.tsx
DESKTOP-TKLFCPRython f29f525c77 refactor: 101.79.17.164 → zioinfo.co.kr 전체 도메인 변환 + Manager UI 배포
- 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>
2026-05-31 10:09:17 +09:00

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 },
})