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

143 lines
6.0 KiB
TypeScript

import React, { useState, useEffect, useRef, useCallback } from 'react'
import {
View, Text, TextInput, TouchableOpacity, FlatList,
StyleSheet, KeyboardAvoidingView, Platform, Alert,
} from 'react-native'
import * as SecureStore from 'expo-secure-store'
import { COLORS, WS_BASE } from '../../constants/Config'
import { getSRChat, sendSRChat } from '../../services/api'
export default function SRChatRoomScreen() {
const [srId, setSrId] = useState('')
const [joined, setJoined] = useState(false)
const [msgs, setMsgs] = useState<any[]>([])
const [input, setInput] = useState('')
const [sending, setSending] = useState(false)
const wsRef = useRef<WebSocket | null>(null)
const flatRef = useRef<FlatList>(null)
const join = useCallback(async () => {
const id = parseInt(srId, 10)
if (!id) { Alert.alert('오류', 'SR 번호를 입력해주세요.'); return }
try {
const r = await getSRChat(id)
setMsgs(r.data?.items ?? r.data ?? [])
setJoined(true)
// WebSocket 연결
const token = await SecureStore.getItemAsync('grd_token')
const ws = new WebSocket(`${WS_BASE}/ws/sr-chat/${id}?token=${token ?? ''}`)
ws.onmessage = e => {
try {
const msg = JSON.parse(e.data)
if (msg.content) setMsgs(prev => [...prev, msg])
} catch {}
}
ws.onerror = () => {}
wsRef.current = ws
} catch { Alert.alert('오류', 'SR 채팅방을 열 수 없습니다.') }
}, [srId])
useEffect(() => () => { wsRef.current?.close() }, [])
const send = async () => {
if (!input.trim() || sending) return
setSending(true)
try {
await sendSRChat(parseInt(srId, 10), input.trim())
setMsgs(prev => [...prev, { id: Date.now(), content: input.trim(), sender: 'me', created_at: new Date().toISOString(), msg_type: 'text' }])
setInput('')
setTimeout(() => flatRef.current?.scrollToEnd(), 100)
} catch {} finally { setSending(false) }
}
if (!joined) {
return (
<View style={s.joinContainer}>
<Text style={s.joinTitle}>SR </Text>
<Text style={s.joinDesc}>SR SR의 .</Text>
<TextInput
style={s.joinInput}
value={srId}
onChangeText={setSrId}
placeholder="SR 번호 입력 (예: 42)"
keyboardType="numeric"
onSubmitEditing={join}
/>
<TouchableOpacity style={s.joinBtn} onPress={join}>
<Text style={s.joinBtnText}> </Text>
</TouchableOpacity>
</View>
)
}
return (
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<View style={s.container}>
<View style={s.header}>
<Text style={s.headerTitle}>SR #{srId} </Text>
<TouchableOpacity onPress={() => { setJoined(false); wsRef.current?.close() }}>
<Text style={s.leave}></Text>
</TouchableOpacity>
</View>
<FlatList
ref={flatRef}
data={msgs}
keyExtractor={(_, i) => String(i)}
contentContainerStyle={{ padding: 12 }}
renderItem={({ item }) => {
const isMe = item.sender === 'me' || item.sender_name === 'me'
return (
<View style={[s.msgWrap, isMe && s.msgWrapRight]}>
{!isMe && <Text style={s.sender}>{item.sender_name ?? item.sender}</Text>}
<View style={[s.bubble, isMe && s.bubbleRight]}>
<Text style={[s.msgText, isMe && s.msgTextRight]}>{item.content}</Text>
</View>
<Text style={s.msgTime}>{item.created_at?.slice(11, 16)}</Text>
</View>
)
}}
/>
<View style={s.inputRow}>
<TextInput
style={s.textInput}
value={input}
onChangeText={setInput}
placeholder="메시지 입력..."
multiline
/>
<TouchableOpacity style={s.sendBtn} onPress={send} disabled={sending}>
<Text style={s.sendBtnText}></Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
)
}
const s = StyleSheet.create({
joinContainer: { flex: 1, backgroundColor: COLORS.bg, padding: 24, justifyContent: 'center' },
joinTitle: { fontSize: 22, fontWeight: '800', color: COLORS.text, marginBottom: 8 },
joinDesc: { fontSize: 14, color: COLORS.muted, marginBottom: 24, lineHeight: 22 },
joinInput: { backgroundColor: '#fff', borderRadius: 10, borderWidth: 1, borderColor: COLORS.border, padding: 14, fontSize: 15, marginBottom: 12 },
joinBtn: { backgroundColor: COLORS.accent, borderRadius: 10, padding: 14, alignItems: 'center' },
joinBtnText: { color: '#fff', fontWeight: '800', fontSize: 15 },
container: { flex: 1, backgroundColor: COLORS.bg },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: COLORS.gnbBg, padding: 14 },
headerTitle: { fontSize: 15, fontWeight: '700', color: '#fff' },
leave: { color: COLORS.accent, fontSize: 14 },
msgWrap: { marginBottom: 10, maxWidth: '80%' },
msgWrapRight: { alignSelf: 'flex-end' },
sender: { fontSize: 11, color: COLORS.muted, marginBottom: 2 },
bubble: { backgroundColor: '#fff', borderRadius: 12, padding: 10, elevation: 1 },
bubbleRight: { backgroundColor: COLORS.accent },
msgText: { fontSize: 14, color: COLORS.text },
msgTextRight: { color: '#fff' },
msgTime: { fontSize: 10, color: COLORS.muted, marginTop: 2 },
inputRow: { flexDirection: 'row', padding: 10, backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: COLORS.border, gap: 8 },
textInput: { flex: 1, backgroundColor: COLORS.bg, borderRadius: 20, paddingHorizontal: 14, paddingVertical: 8, fontSize: 14, maxHeight: 100 },
sendBtn: { backgroundColor: COLORS.accent, borderRadius: 20, paddingHorizontal: 16, justifyContent: 'center' },
sendBtnText: { color: '#fff', fontWeight: '700' },
})