143 lines
6.0 KiB
TypeScript
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' },
|
|
})
|