-
아마존 E-commerce 클론 -33) 채팅 기능 만들기(socket io)NODE.JS 2021. 5. 30. 22:39
socket io는 자바스크립 라이브러리이고 리얼타임 인터랙션을 만들어내는 라이브러리다.
최상위 폴더에 npm install socket.io
server.js에 다음과 같이 입력해준다.
import http from 'http' import {Server} from 'socket.io' const httpServer = http.Server(app); const io = new Server(httpServer,{cors:{origin:'*'}})//오류방지 const users = []; io.on('connection', (socket)=>{//연결끊기 socket.on('disconnect',()=>{ const user = users.find(x=>x.socketId===socket.id); if(user){ user.online = false; console.log('Offline', user.name); const admin = users.find(x=>x.isAdmin && x.online); if(admin){ io.go(admin.socketId).emit('updateUser', user) } } }); socket.on('onLogin', (user)=>{//새로운 유저가 들어왔을때(연결하기) const updatedUser = { ...user, online:true, socketId:socket.id, messages:[] }; const existUser = users.find(x=>x._id ===updatedUser._id); if(existUser){ existUser.socketId = socket.id; existUser.online = true; }else{ users.push(updatedUser); } console.log('Online', user.name); const admin = users.find(x=>x.isAdmin&& x.online); if(admin){ io.to(admin.socketId).emit('updateUser', updatedUser) } if(updatedUser.isAdmin){ io.to(updatedUser.socketId).emit('listUsers', users) } }); socket.on('onUserSelected', (user)=>{ const admin = users.find(x=>x.isAdmin&& x.online) if(admin){ const existUser = users.find(x=>x._id===user._id); io.to(admin.socketId).emit('selectedUser', existUser) } }) socket.on('onMessage', (message)=>{ if(message.isAdmin){ const user = users.find(x=>x._id===message._id && x.online) if(user){ io.to(user.socketId).emit('message', message) user.messages.push(message); } }else{ const admin = users.find(x=>x.isAdmin&& x.online) if(admin){ io.to(admin.socketId).emit('message', message) const user = users.find(x=>x._id===message._id && x.online) user.messages.push(message) }else{ io.to(socket.id).emit('message',{ name:'Admin', body:'Sorry.I am not online right now.' }) } } }) }) httpServer.listen(port, ()=>{ onsole.log(`Serve at http://localhost:${port}`); })
채팅창을 만들어보자.
Admin에게 만들어 줄것이다.
SupportScreen.js
import React from 'react' import MessageBox from '../components/MessageBox' function SupportScreen() { return ( <div className="row top full-container"> <div className="col-1 support-users"> {users.filter((x) => x._id !== userInfo._id).length === 0 && ( <MessageBox>No Online User Found</MessageBox> )} <ul> {users .filter((x) => x._id !== userInfo._id) .map((user) => ( <li key={user._id} className={user._id === selectedUser._id ? ' selected' : ' '} > <button className="block" type="button" onClick={() => selectUser(user)} > {user.name} </button> <span className={ user.unread ? 'unread' : user.online ? 'online' : 'offline' } /> </li> ))} </ul> </div> <div className="col-3 support-messages"> {!selectedUser._id ? ( <MessageBox>Select a user to start chat</MessageBox> ) : ( <div> <div className="row"> <strong>Chat with {selectedUser.name} </strong> </div> <ul ref={uiMessagesRef}> {messages.length === 0 && <li>No message.</li>} {messages.map((msg, index) => ( <li key={index}> <strong>{`${msg.name}: `}</strong> {msg.body} </li> ))} </ul> <div> <form onSubmit={submitHandler} className="row"> <input value={messageBody} onChange={(e) => setMessageBody(e.target.value)} type="text" placeholder="type message" /> <button type="submit">Send</button> </form> </div> </div> )} </div> </div> ); } export default SupportScreen
이제 스테잇을 가져와보자.
여기서 소켓이 없으면 생성하는 라이브러리를 다운받자.
frontend로 가서 npm install socket.io-client
import socketIOClient from 'socket.io-client' let allUsers = []; let allMessages = []; let allSelectedUser= []; const ENDPOINT = window.location.host.indexOf('localhost')>=0? 'http://127.0.0.1:5000' : window.location.host; function SupportScreen() { const [selectedUser, setSelectedUser] = useState({}); const [socket, setSocket] = useState(null); const uiMessagesRef = useRef(null); const [messageBody, setMessageBody] = useState(''); const [messages, setMessages] = useState([]); const [users, setUsers] = useState([]); const userSignin = useSelector((state) => state.userSignin); const { userInfo } = userSignin; useEffect(() => { if (uiMessagesRef.current) {//메세지 제일 최근것으로 내려간다. uiMessagesRef.current.scrollBy({ top: uiMessagesRef.current.clientHeight, left: 0, behavior: 'smooth', }); } if (!socket) { const sk = socketIOClient(ENDPOINT);//소켓이 없다면 생성해준다. setSocket(sk); sk.emit('onLogin', {//서버의 onLogin을 발생시킨다. (server.js) _id: userInfo._id, name: userInfo.name, isAdmin: userInfo.isAdmin, }); sk.on('message', (data) => { if (allSelectedUser._id === data._id) {//이미 메세지 보낸적이 있다면 allMessages = [...allMessages, data];//메세지 목록에 추가해준다. } else { const existUser = allUsers.find((user) => user._id === data._id);//사용자를 찾는다. if (existUser) { allUsers = allUsers.map((user) => user._id === existUser._id ? { ...user, unread: true } : user ); setUsers(allUsers); } } setMessages(allMessages); }); sk.on('updateUser', (updatedUser) => { const existUser = allUsers.find((user) => user._id === updatedUser._id); if (existUser) { allUsers = allUsers.map((user) => user._id === existUser._id ? updatedUser : user ); setUsers(allUsers); } else { allUsers = [...allUsers, updatedUser]; setUsers(allUsers); } }); sk.on('listUsers', (updatedUsers) => { allUsers = updatedUsers; setUsers(allUsers); }); sk.on('selectUser', (user) => { allMessages = user.messages; setMessages(allMessages); }); } }, [messages, socket, users]);
이제 함수들을 만들어 보자.
const selectUser = (user)=>{ allSelectedUser = user; setSelectedUser(allSelectedUser) const existUser = allUsers.find(x=>x._id===user._id) if(existUser){ allUsers = allUsers.map(x=>{ x._id===existUser._id? {...x, unread:false}:x }) setUsers(allUsers) } socket.emit('onUserSelected', user) } const submitHandler = (e) => { e.preventDefault(); if (!messageBody.trim()) { alert('Error. Please type message.'); } else { allMessages = [ ...allMessages, { body: messageBody, name: userInfo.name }, ]; setMessages(allMessages); setMessageBody(''); setTimeout(() => { socket.emit('onMessage', { body: messageBody, name: userInfo.name, isAdmin: userInfo.isAdmin, _id: selectedUser._id, }); }, 1000); } };
app.js에 페이지 추가
import SupportScreen from './screens/SupportScreen'; {userInfo && userInfo.isAdmin && ( <div className="dropdown"> <Link to="#admin">Admin {' '}<i className="fa fa-caret-down"></i></Link> <ul className="dropdown-content"> <li> <Link to = "/dashboard">Dashboard</Link> </li> <li> <Link to = "/productlist">Products</Link> </li> <li> <Link to = "/orderlist">Orders</Link> </li> <li> <Link to = "/userlist">Users</Link> </li> <li> <Link to = "/support">Support</Link> </li> </ul> </div> <AdminRoute path="/support" component={SupportScreen} exact></AdminRoute>
이제 홈페이지에 채팅 박스를 넣어보자.
components>ChatBox.js
import React, { useEffect, useRef, useState } from 'react'; import socketIOClient from 'socket.io-client'; const ENDPOINT = window.location.host.indexOf('localhost') >= 0 ? 'http://127.0.0.1:5000' : window.location.host; export default function ChatBox(props) { const { userInfo } = props; const [socket, setSocket] = useState(null); const uiMessagesRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [messageBody, setMessageBody] = useState(''); const [messages, setMessages] = useState([ { name: 'Admin', body: 'Hello there, Please ask your question.' }, ]); useEffect(() => { if (uiMessagesRef.current) { uiMessagesRef.current.scrollBy({//chatbox있으면 스크롤 다운 top: uiMessagesRef.current.clientHeight, left: 0, behavior: 'smooth', }); } if (socket) { socket.emit('onLogin', { _id: userInfo._id, name: userInfo.name, isAdmin: userInfo.isAdmin, }); socket.on('message', (data) => {//새 메세지 보내기 setMessages([...messages, { body: data.body, name: data.name }]); }); } }, [messages, isOpen, socket]); const supportHandler = () => { setIsOpen(true); console.log(ENDPOINT); const sk = socketIOClient(ENDPOINT); setSocket(sk); }; const submitHandler = (e) => {//새메시지 보내기 e.preventDefault(); if (!messageBody.trim()) { alert('Error. Please type message.'); } else { setMessages([...messages, { body: messageBody, name: userInfo.name }]); setMessageBody(''); setTimeout(() => {//1초동안 socket.emit('onMessage', { body: messageBody, name: userInfo.name, isAdmin: userInfo.isAdmin, _id: userInfo._id, }); }, 1000); } }; const closeHandler = () => { setIsOpen(false); }; return ( <div className="chatbox"> {!isOpen ? ( <button type="button" onClick={supportHandler}> <i className="fa fa-support" /> </button> ) : ( <div className="card card-body"> <div className="row"> <strong>Support </strong> <button type="button" onClick={closeHandler}> <i className="fa fa-close" /> </button> </div> <ul ref={uiMessagesRef}> {messages.map((msg, index) => ( <li key={index}> <strong>{`${msg.name}: `}</strong> {msg.body} </li> ))} </ul> <div> <form onSubmit={submitHandler} className="row"> <input value={messageBody} onChange={(e) => setMessageBody(e.target.value)} type="text" placeholder="type message" /> <button type="submit">Send</button> </form> </div> </div> )} </div> ); }
채팅박스 스타일하자.
/* Chatbox */ .chatbox { color: #000000; position: fixed; right: 1rem; bottom: 1rem; } .chatbox ul { overflow: scroll; max-height: 20rem; } .chatbox li { margin-bottom: 1rem; } .chatbox input { width: calc(100% - 9rem); } .support-users { background: #f0f0f0; height: 100%; } .support-users li { background-color: #f8f8f8; } .support-users button { background-color: transparent; border: none; text-align: left; } .support-users li { margin: 0; background-color: #f0f0f0; border-bottom: 0.1rem #c0c0c0 solid; } .support-users li:hover { background-color: #f0f0f0; } .support-users li.selected { background-color: #c0c0c0; } .support-messages { padding: 1rem; } .support-messages input { width: calc(100% - 9rem); } .support-messages ul { height: calc(100vh - 18rem); max-height: calc(100vh - 18rem); overflow: scroll; } .support-messages li { margin-bottom: 1rem; } .support-users span { width: 2rem; height: 2rem; border-radius: 50%; position: absolute; margin-left: -25px; margin-top: 10px; } .support-users .offline { background-color: #808080; } .support-users .online { background-color: #20a020; } .support-users .unread { background-color: #f02020; }
app.js에 추가
import ChatBox from './components/ChatBox'; <footer className="row center"> {userInfo && !userInfo.isAdmin && <ChatBox userInfo={userInfo}></ChatBox>} <div>All right reserved</div> {' '} </footer>
일반계정에서 메세지를 보내고 다시 관리자 계정으로 오면,
여기 진짜 엉망진창 나중에 꼭 다시 봐야한다.
'NODE.JS' 카테고리의 다른 글
Social Media 만들기 - 3) Redux , RefreshToken (Login 기능 만들기) (0) 2021.07.05 Social Media 만들기 - 2) Authentification 기능 만들기 (0) 2021.06.16 아마존 E-commerce 클론 -32) 차트 스크린 만들기 (0) 2021.05.30 아마존 E-commerce 클론 -31) Order Receipt 이메일로 보내기 (0) 2021.05.30 아마존 E-commerce 클론 -30) Pagination적용하기 (0) 2021.05.30