NODE.JS

아마존 E-commerce 클론 -33) 채팅 기능 만들기(socket io)

dodop 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>

 

관리자가 아닌 계정으로 들어가면 메세지 박스를 확인할 수 있다. 

일반계정에서 메세지를 보내고 다시 관리자 계정으로 오면, 

 

 

 

 

 


여기 진짜 엉망진창 나중에 꼭 다시 봐야한다.