ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아마존 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>

     

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

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

     

     

     

     

     


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

     

     

     

     

     

     

     

     

     

     

Designed by Tistory.