ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Social Media 만들기 - 21) message 기능 만들기 (intersection Observer(infiniteScrolling) , online-offline 정보 가져오기)
    NODE.JS 2021. 8. 7. 23:42

    이제 메세지 페이지를 구성하는 메세지들을 실시간으로 오고가도록 설정하자. 

     

    먼저 하나의 메세지 내용을 담는 메세지 모델과, 

     

    대화내용의 마지막 파트를 저장하고 있는 conversation모델을 만들것이다. 

    (메세지왼쪽 부분에 가장 최근에 주고받은 메세지들 저장하기 위해서)

    messagemodel

    const mongoose = require('mongoose')
    
    const messageSchema = new mongoose.Schema({
        conversation:{
            type:mongoose.Types.ObjectId,
            ref:'Conversation'
        },
        sender:{
            type:mongoose.Types.ObjectId,
            ref:'User'
        },
        recipient:{
            type:mongoose.Types.ObjectId,
            ref:'User'
        },
        text:'String',
        media:Array,
        call:Object
    
    },{timestamps:true})
    
    const Message = mongoose.model('Message', messageSchema)
    module.exports  = {Message}

     

     

    conversationmodel

    const mongoose = require('mongoose')
    
    const conversationSchema = new mongoose.Schema({
        recipients:[{
            type:mongoose.Types.ObjectId,
            ref:'User'
        }],
        text:'String',
        media:Array,
        call:Object
    
    },{timestamps:true})
    
    const Conversation = mongoose.model('Conversation', conversationSchema)
    module.exports  = {Conversation}

     

     

     

    이제 메세지 파트를 만들도록 하자. 

    메세지 페이지에서 왼쪽 부분과 오른쪽 부분으로 나뉘도록 설정해준다. 

    이 메세지 페이지는 가장 먼저 메세지를 누르면 나오는 페이지이고, 

    왼쪽 에는 대화를 나눈 사람들의 목록을 나타나도록 할 것이다. 

    message(page).js

    import React from 'react'
    import { useSelector } from 'react-redux'
    import LeftSide from '../component/messages/LeftSide'
    import RightSide from '../component/messages/RightSide'
    
    function Message(props) {
        const userLogin = useSelector(state => state.userLogin)
        const userInfo = userLogin
    
        if(!userInfo){
            props.history.push('/login')
        }
        return (
            <div className="message d-flex">
                <div className="col-md-4 border-right px-0 left-mess">
                    <LeftSide/>
                </div>
                {/* <div className="col-md-8 px-0">
                    <RightSide/>
                </div> */}
                <div className="col-md-8 px-0 right_mess">
                    <div className="d-flex justify-content-center align-items-center flex-column h-100">
                        <i className="fab fa-facebook-messenger text-primary" style={{fontSize:'5rem'}}></i>
                        <h4>Messenger</h4>
                    </div>
                </div>
            </div>
        )
    }
    
    export default Message

    이런모양

     

    이제 실제적으로 대화목록을 클릭해서 특정 대화상대와의 대화창으로 가는 페이지를 만들자. 

    component>message>messge.js를 만들어줄것이다. 

    먼저 app.js에 링크를 추가해서 페이지가 존재하는 것을 알려주자. 

              <PrivateRouter exact path="/message/:id" component={Message}/>

    message.js

    여기도 왼쪽 부분과 오른쪽 부분으로 나뉠 것이다. 

    주소로 받은 id 정보를 prop으로 넘겨준다. 

    import React, { useEffect } from 'react'
    import { useDispatch } from 'react-redux'
    import { getMessage } from '../../_actions/messageActions'
    import LeftSide from './LeftSide'
    import RightSide from './RightSide'
    
    function Message(props) {
        const id =  props.match.params.id
    
        return (
            <div className="message d-flex">
                <div className="col-md-4 border-right px-0 left-mess">
                    <LeftSide id={id}/>
                </div>
    
                <div className="col-md-8 px-0">
                    <RightSide id={id}/>
                </div>
    
            </div>
        )
    }
    
    export default Message

    먼저 왼쪽 파트를 구성하자. 

    왼쪽 파트는 실제 사용하고있는 본인의 아이디를 이용해서, 

    conversation을 가지고 온다음, 화면에 보이도록 설정할 것이다. 

    먼저 대화 정보들을 가져오도록 messageRouter를 작성해주자. 

    우선 server.js에 라우트를 추가해준다. 

    app.use('/api/message', require('./routes/messageRouter'));

     

    messageRouter.js

    messageRouter.get('/', auth, async(req,res)=>{
        try{
            const conversations = await Conversation.find({
                recipients:req.user.id
            }).sort('-createdAt')
                .populate('recipients', 'avatar username fullname')
    
            
            res.json({
                conversations
            })
        }catch(err){
            return res.status(500).json({msg:err.message})
        }
    })

     

    messageConstants.js

    export const CONVERSATION_GET_REQUEST = 'CONVERSATION_GET_REQUEST'
    export const CONVERSATION_GET_SUCCESS = 'CONVERSATION_GET_SUCCESS'
    export const CONVERSATION_GET_FAIL = 'CONVERSATION_GET_FAIL'

    messageActions.js

    여기서 새로운 어레이를 만드는 이유는, 

    대화의 수신자들이 [user1, user2] 이러한 방식으로 2명이 들어가 있기 때문이다. 

    나 자신이 아닌, 지금 대화창에 있는 상대방 정보가 필요한 것이기 때문에, 나를 제외한 상대의 정보를 user로서 담는다. 

    (아바타, 이름...의 정보)

    export const getConversation = ()=>async(dispatch, getState)=>{
    
        const {userLogin:{userInfo}} = getState()
    
        dispatch({
            type:CONVERSATION_GET_REQUEST
        })
        try{
            const res = await axios.get('/api/message', {
                headers:{authorization:`Bearer ${userInfo.token}`}
            })
    
            const newArr=[]
    
            res.data.conversations.forEach(item=>(
                item.recipients.forEach(receiver=>{
                    if(receiver._id!==userInfo.user._id){
                        newArr.push({...receiver, text:item.text, media:item.media, call:item.call})
                    }
                })
            ))
    
            dispatch({
                type:CONVERSATION_GET_SUCCESS,
                payload:newArr
            })
    
        }catch(error){
            dispatch({
                type:CONVERSATION_GET_FAIL,
                payload:                
                error.response && error.response.data.message
                ? error.response.data.message
                : error.message
            })
        }
    }

     

    messageReducer

    user들을 가져왔는지 확인할 수 있도록 firstLoad조건도 설정해준다. 

    import { CHECK_ONLINE, CONVERSATION_DELETE_REQUEST, CONVERSATION_DELETE_SUCCESS, CONVERSATION_GET_FAIL, CONVERSATION_GET_REQUEST, CONVERSATION_GET_SUCCESS, MESSAGE_ADD_FAIL, MESSAGE_ADD_REQUEST, MESSAGE_ADD_SUCCESS, MESSAGE_ADD_USER, MESSAGE_DELETE_FAIL, MESSAGE_DELETE_REQUEST, MESSAGE_DELETE_SUCCESS, MESSAGE_GET_FAIL, MESSAGE_GET_MORE_FAIL, MESSAGE_GET_MORE_REQUEST, MESSAGE_GET_MORE_SUCCESS, MESSAGE_GET_REQUEST, MESSAGE_GET_SUCCESS } from "../_constants/messageConstants";
    
    export const messageReducer = (state={ users:[], data:[]}, action)=>{
        switch(action.type){
        
            case CONVERSATION_GET_REQUEST:
                return {...state, loading:true}
            case CONVERSATION_GET_SUCCESS:
                return {...state, loading:false, firstLoad: true, users:action.payload}
            case CONVERSATION_GET_FAIL:
                return {...state, loading:false, error:action.payload}
    
    }

    store

        message:messageReducer,

    추가로 지금 사용자가 로그인한 상태인지 아닌지를 확인하기 위해서 

    online정보를 소켓에서 받아오도록 작성해줄것이다. 

    소켓을 이용해서 사용자가 online이면 스테잇에 추가해준다. 

     

    checkOnlineConstant

    export const ONLINE = 'ONLINE'
    export const OFFLINE = 'OFFLINE'

     

    onlineCheckReducers.

    import { OFFLINE, ONLINE } from "../_constants/onlineCheckConstants";
    
    export const onlineReducer = (state=[], action)=>{
        switch(action.type){
            case ONLINE:
                return [...state, action.payload ]
            case OFFLINE:
                return state.filter(item=>item !== action.payload)
            default:
                return state
        }
    }

    store

        online:onlineReducer,

     

    socketClient.js

    첫번째로는 내가 팔로우하고 있는 사람들의 서버사용정보를 받는다. 

    사용자가 로그인하면 checkuseronline을 일으켜서 사용자의 정보를 서버에 넘겨주고, 

    서버는 로그인한 사용자가 팔로잉하는사람들 중에서 서버에 로그된 사람들만 다시 넘겨주고, 

    넘겨받은 정보를 클라이언트는 online스테잇으로 저장한다. 

     

    두번째는 나를 팔로우하고 있는 사람들에게 내가 로그인했음을 알린다. 

    서버에서 로그인한 유저의 정보를 이용해서 유저를 팔로우하고 있으면서 서버사용중인 사람들에게,

    방금 로그인한 유저의 아이디 정보를 알려주고, 

    클라이언트는 그 정보를 받아서 만약, online스테잇에 그사람이 없다면 추가해서 등록하도록 설정한다. 

     

        //Check User Online
        useEffect(() => {
            socket.emit('checkUserOnline', userInfo.user)
        }, [socket, userInfo.user])
    
        useEffect(() => {
            socket.on('checkUserOnlineToMe', data=>{
                data.forEach(item=>{
                    if(!online.includes(item.id)){
                        dispatch({
                            type:ONLINE,
                            payload:item.id
                        })
                    }
                })
            })
            return () => socket.off('checkUserOnlineToMe')
        }, [socket, dispatch, online])
        
    
        useEffect(() => {
            socket.on('checkUserOnlineToClient', data=>{
                if(!online.includes(data)){
                    dispatch({
                        type:ONLINE,
                        payload:data
                    })
                }
                
            })
            return () => socket.off('checkUserOnlineToClient')
        }, [socket, dispatch, online])

    socketServer.js

        //Check User Online/Offline
        socket.on('checkUserOnline', data=>{
            // console.log(user)
            const following = users.filter(user=>data.following.find(item=>item._id ===user.id))
    
            socket.emit('checkUserOnlineToMe', following)
    
            const clients = users.filter(user=>data.followers.find(item => item._id ===user.id))
            if(clients.length >0){
                clients.forEach(client=>{
                    socket.to(`${client.socketId}`).emit('checkUserOnlineToClient', data._id)
                })
            }
        })

     

    오프라인도 정보를 주고받자. 

     

    오프라인을 사용하면, 유저의 online 정보를 바꾸게 된다. 

    message를 보여주는 왼쪽 사이드부분에 이를 사용하므로, 

    state>messge>user의 정보에 online정보를 추가해서 활용할 것이다. 

    messageConstants

    export const CHECK_ONLINE = 'CHECK_ONLINE'

     

    messageReducer

            case CHECK_ONLINE:
                return {...state, 
                        users:state.users.map(user=>
                                action.payload.includes(user._id)
                                ? {...user, online:true}
                                : {...user, online:false}
                            )}

     

    socket.server

    let users = []
    
    const SocketServer = (socket) =>{
    
        //Connect -Disconnect
        socket.on('joinUser', user=>{
            users.push({id:user._id, socketId:socket.id, followers:user.followers})
            // console.log({users})
            // console.log({users})
        } )
    
        socket.on('disconnect', ()=>{
            const data = users.find(user=>user.socketId === socket.id)
            if(data){
                const clients = users.filter(user=>data.followers.find(item=>item._id ===user.id))
                if(clients.length>0){
                    clients.forEach(client=>{
                        socket.to(`${client.socketId}`).emit('checkUserOffline', data.id)
                    })
                }
            }
            users = users.filter(user=>user.socketId !== socket.id)
            // console.log({users})
        })

     

    socket.client

        //check user off line
        useEffect(() => {
            socket.on('checkUserOffline', id=>{
                if(online.includes(id)){
                    dispatch({
                        type:OFFLINE,
                        payload:id
                    })
                }
            })
    
            return () => socket.off('checkUserOffline')
    
        }, [socket, dispatch, online])
        return <> </>

    추가로 대화방이 존재하지 않는 유저를 검색해서 클릭했을 때, 

    새로운 대화를 만드는 것을 작성하자. 

    messageconstant

    export const MESSAGE_ADD_USER='MESSAGE_ADD_USER'

     

    messageReducer

     case MESSAGE_ADD_USER:
                if(state.users.every(item => item._id !== action.payload._id)){
                    return {...state, users:[action.payload,...state.users]}
                }
                return state;

     

     

    이제 정보는 다 받아왔다. 

    leftside에서 사용해주자. 

    search기능은 Header에서 사용했던 방식을 그대로 가져왔다. 

     

    대화목로에 없는 유저를 검색해서 클릭하면, 

    새로운 유저를 state에 추가해준다. 

     

    userCard를 클릭하면 해당메세지 링크로 넘어간다. 

    (/message/:id)

    팔로우하는 유저들의 서버접속 여부를 확인할 수 있고,

    사용자의 온라인 정보에 따라서 동그라미의 체크 표시가달라진다. 

     

    leftside.js

    import axios from 'axios'
    import React, { useEffect, useState } from 'react'
    import { useDispatch, useSelector } from 'react-redux'
    import { useHistory } from 'react-router-dom'
    import { getConversation } from '../../_actions/messageActions'
    import { ALERT } from '../../_constants/globalConstants'
    import { CHECK_ONLINE, MESSAGE_ADD_USER } from '../../_constants/messageConstants'
    import UserCard from '../UserCard'
    
    function LeftSide({id}) {
    
    
        const userLogin = useSelector(state => state.userLogin)
        const {userInfo} = userLogin
    
        const message = useSelector(state => state.message)
    
        const online = useSelector(state => state.online)
    
        const [search, setSearch] = useState("")
        const [load, setLoad] = useState(false)
        const [users, setUsers] = useState([])
    
        const dispatch = useDispatch()
        const history = useHistory()
    
        useEffect(() => {
            if(message.firstLoad) return;
            dispatch(getConversation())
        }, [dispatch, message.firstLoad])
    
        const handleSearch=(e)=>{
            e.preventDefault()
    
            if(search && userInfo.token){
                async function get(){
                    await axios.get(`/api/search?username=${search}`,{headers:{authorization : `Bearer ${userInfo.token}`}} )
                                .then(res=>setUsers(res.data.users.filter(user=>user._id !== userInfo.user._id)))
                                .catch(err=>{
                                    dispatch({
                                        type:ALERT,
                                        payload:{error:err.response.data.message}
                                    })
                                })
                }
                setLoad(true)
                get();
                setLoad(false)
            }else{
                setUsers([])
            }
            
        }
    
        const handleAddUser=(user)=>{
            const existingUser = message.users.filter(item=>item._id===user._id)
            console.log(existingUser)
            if(existingUser.length===0){
                dispatch({
                    type:MESSAGE_ADD_USER,
                    payload:{...user, text:'', media:[]}
                })
            }
            
    
            setSearch('')
            setUsers([])
            return history.push(`/message/${user._id}`)
    
        }
    
        const isActive=(user)=>{
            if(id===user._id) return 'active'
            return ''
        }
    
        //check user online/offline
        useEffect(() => {
            if(message.firstLoad){
                dispatch({
                    type:CHECK_ONLINE,
                    payload:online
                })
            }
        }, [online, message.firstLoad, dispatch])
    
        return (
            <>
                <form className="message_header" onSubmit={handleSearch}>
                    <input type="text" value={search} placeholder="Enter to Search" onChange={e=>setSearch(e.target.value)}/>
                    <button type="submit" style={{display:'none'}}>Search</button>
                </form>
    
                <div className="message_chat_list">
                    {
                        users.length !== 0
                        ? <>
                            {
                                users.map(user=>(
                                    <div key={user._id} className={`message_user ${isActive(user)}`} onClick={()=>handleAddUser(user)}>
                                        <UserCard user={user} id={user._id}  />
                                    </div>
                                )) 
                            }
                        </>
                        : <>
                        {
                            message.users.map(user=>(
                                <div key={user._id} className={`message_user ${isActive(user)}`} onClick={()=>handleAddUser(user)}>
                                    <UserCard user={user} id={user._id} msg={true}>
                                        {
                                            user.online 
                                            ?<i className="fas fa-circle text-success"></i>
                                            : userInfo.user.following.find(item=>item._id === user._id)
                                             && <i className="fas fa-circle"></i>
    
                                        }
                                    </UserCard>
                                </div>
                            ))
                        }
                        </>
                    }
                </div>
            </>
        )
    }
    
    export default LeftSide

    message.css

    .message{
        width: 100%;
        height: calc(100vh - 100px);
        border: 1px solid #ddd;
        margin-top: 15px;
        border-radius: 3px;
        background: #fbfbfb;
    }
    
    .message_header{
        width: 100%;
        height: 60px;
        border-bottom: 1px solid #ddd;
        display: flex;
        justify-content: space-between;
        align-items: center;
        background: #f8f8f8;
    }
    .message_header input{
        flex: 1;
        height: 100%;
        border: none;
        outline: none;
        background: #f8f8f8;
        padding: 0 5px;
    }
    .message_chat_list{
        width: 100%;
        height: calc(100% - 60px);
        overflow-y: auto;
    }
    .message_chat_list .message_user{
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 5px 10px;
        border: 1px solid #f3f3f3;
        color: #555;
        cursor: pointer;
    }
    .message_chat_list .message_user.active{
        background: #eee;
    }
    
    .message_chat_list .message_user a{
        color: #555;
    }
    .message_chat_list .message_user a:hover{
        text-decoration: none;
    }
    
    .message_chat_list .message_user .fa-circle{
        font-size: 8px;
        color: #aaa;
    }
    .message_chat_list .message_user .fa-circle.active{
        color: forestgreen;
    }
    
    /* -------- Chat Input----- */
    .chat_input{
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0 10px;
        border-top: 1px solid #ddd;
    }
    .chat_input input{
        width: 100%;
        height: 49px;
        border: none;
        outline: none;
    }
    .chat_input button{
        border: none;
        outline: none;
        background: white;
    }
    
    .chat_input .file_upload{
        position: relative;
        overflow: hidden;
        margin: 0 10px;
    }
    .chat_input .file_upload #file{
        position: absolute;
        top:0;
        left: 0;
        opacity: 0;
    }
    .show_media{
        width: 100%;
        height: 70px;
        overflow: hidden;
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
        place-items: center;
        grid-gap: 10px;
        background: #f3f3f3;
        border-radius: 5px;
        padding: 0 5px;
    }
    .show_media #file_media{
        position: relative;
        width: 100%;
        height: 100%;
        max-width: 70px;
        max-height: 70px;
    }
    .show_media #file_media img,
    .show_media #file_media video{
        width: 100%;
        height: 100%;
        display: block;
        object-fit: contain;
    }
    .show_media #file_media span{
        position: absolute;
        top: 0;
        right: 0;
        z-index: 4;
        background: white;
        border: 1px solid crimson;
        padding: 3px 7px;
        color: crimson;
        border-radius: 50%;
        font-size: 10px;
        font-weight: bold;
        cursor: pointer;
    }
    
    /* -------- Chat Container ----- */
    .chat_container{
        width: 100%;
        height: calc(100% - 110px);
        overflow-y: auto;
        padding: 0 10px;
    }
    .chat_display{
        width: 100%;
        min-height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
    }
    .chat_row{
        display: grid;
        grid-template-columns: 70%;
        margin-bottom: 10px;
    }
    .you_message{
        justify-content: end;
        justify-items: end;
    }
    .other_message{
        justify-content: start;
        justify-items: start;
    }
    .chat_text{
        padding: 9px 14px;
        margin-bottom: 5px;
    }
    .chat_time{
        font-size: 13px;
        color: #777;
    }
    .you_message .chat_text{
        background: #0048AA;
        color: white;
        border: 1px solid #0048AA;
        border-radius: 14px 14px 0 14px;
    }
    .other_message .chat_text{
        background: white;
        color: #111;
        border: 1px solid #ddd;
        border-radius: 14px 14px 14px 0;
    }
    .chat_title{
        margin-bottom: 3px;
    }
    .you_content{
        position: relative;
    }
    .you_content .fa-trash{
        position: absolute;
        top: 50%;
        left: -15px;
        transform: translateY(-50%);
        cursor: pointer;
        font-size: 14px;
        opacity: 0;
    }
    .you_content:hover .fa-trash{
        opacity: 1;
    }
    .chat_input .dropdown-menu{
        transform: translate3d(-110px, -200px, 0px) !important;
    }

     

     

    이제 오른쪽사이드를 구성하자. 

     

    먼저 메세지를 가져오고, 

    메세지를 삭제하고, 

    대화방 자체를 삭제하고,

    infinite scrolling을 이용해서 위로 올리면, 더 많은 대화들을 가져오도록 설정할 것이다. 

    (intersectionObserver)

     

    router를 먼저 작성해준다. 

    새로운 메세지를 작성할때는, 기존에 존재하던 대화를 찾아서  업데이트해서 최신 메세지 내용만 저장하도록 설정한다. 

    메세지는 최신대화아이디, 보내는사람, 받는사람, 텍스트, 이미지등의 미디어 정보를 가지고 있다. 

     

    loadmore을 구현할 때는, page 정보를 받아와서 limit* page개의 정보를 받아와서 메세지 정보를 교체해준다. 

     

    대화방자체를 삭제시에는 해당최신대화아이디와 연관되어있는 모든 메세지들을 삭제해줘야 한다. 

    const express = require('express')
    const {auth} = require('../middleware/auth')
    const {Message} = require('../models/Message')
    const {Conversation} = require('../models/Conversation')
    
    
    const messageRouter = express.Router()
    
    messageRouter.post('/', auth, async(req, res)=>{
        try{
            const {recipient, text, media} = req.body
    
            if(!recipient || (!text.trim() && media.length===0))  return;
    
            const newConversation = await Conversation.findOneAndUpdate({
                $or:[
                    {recipients:[req.user.id, recipient]},
                    {recipients:[recipient, req.user.id]}
                ]
            },{
                recipients:[req.user.id, recipient],
                text, media
            },{new:true, upsert:true})
    
    
            const newMessage = new Message({
                conversation:newConversation,
                sender:req.user.id,
                recipient,
                text,
                media
            })
            await newMessage.save()
    
            res.json({
                msg:'Create Success.'
            })
    
        }catch(err){
            return res.status(500).json({msg:err.message})
        }
    })
    
    messageRouter.get('/:id', auth, async(req, res)=>{
        try{
    
            const limit = req.query.limit || 9
            const page = req.query.page || 1
    
            const messages = await Message.find({
                $or:[
                    {sender:req.user.id, recipient:req.params.id},
                    {sender:req.params.id, recipient:req.user.id}
                ]
            }).sort('-createdAt')
                .limit(Number(page)* Number(limit))
    
            
    
            res.json({
                messages,
                result:messages.length
            })
        }catch(err){
            return res.status(500).json({msg:err.message})
        }
    })
    
    
    messageRouter.delete('/:id', auth, async(req, res)=>{
        try{
            const deletedMessage = await Message.findOneAndDelete({_id:req.params.id, sender:req.user.id})
    
            res.json({deletedMessage})
    
        }catch(err){
            return res.status(500).json({msg:err.message})
        }
    })
    
    messageRouter.delete('/conversation/:id', auth , async(req, res)=>{
        try{
            const deletedConversation = await Conversation.findOneAndDelete({
                $or:[
                    {recipients: [req.user.id, req.params.id]},
                    {recipients: [req.params.id, req.user.id]}
                ]
            })
    
            await Message.deleteMany({conversation: deletedConversation._id})
    
            res.json({deletedConversation})
    
        }catch(err){
            return res.status(500).json({msg:err.message})
        }
    })
    
    module.exports = messageRouter

     

    먼저 message constants

    
    export const MESSAGE_ADD_REQUEST = 'MESSAGE_ADD_REQUEST'
    export const MESSAGE_ADD_SUCCESS = 'MESSAGE_ADD_SUCCESS'
    export const MESSAGE_ADD_FAIL = 'MESSAGE_ADD_FAIL'
    
    export const MESSAGE_GET_REQUEST = 'MESSAGE_GET_REQUEST'
    export const MESSAGE_GET_SUCCESS = 'MESSAGE_GET_SUCCESS'
    export const MESSAGE_GET_FAIL = 'MESSAGE_GET_FAIL'
    
    export const MESSAGE_GET_MORE_REQUEST = 'MESSAGE_GET_MORE_REQUEST'
    export const MESSAGE_GET_MORE_SUCCESS = 'MESSAGE_GET_MORE_SUCCESS'
    export const MESSAGE_GET_MORE_FAIL = 'MESSAGE_GET_MORE_FAIL'
    
    export const MESSAGE_DELETE_REQUEST = 'MESSAGE_DELETE_REQUEST'
    export const MESSAGE_DELETE_SUCCESS = 'MESSAGE_DELETE_SUCCESS'
    export const MESSAGE_DELETE_FAIL = 'MESSAGE_DELETE_FAIL'
    
    export const CONVERSATION_DELETE_REQUEST = 'CONVERSATION_DELETE_REQUEST'
    export const CONVERSATION_DELETE_SUCCESS = 'CONVERSATION_DELETE_SUCCESS'
    export const CONVERSATION_DELETE_FAIL = 'CONVERSATION_DELETE_FAIL'

     

     

    messageAction

    여기서 메세지를 추가해줄 때에는 , 

    메세지의 미디어 정보를 다음과 같이 bodyForm을 이용해서 형식을 가지고 메세지 폴더에 저장해줄 것이다. 

     

    메세지 정보를 가져올때는, 순서를 -createAt으로 해서 끝에서부터 limit개의 갯수를 가져왔지만, 

    보여줄 때는 다시 원래순서대로 보여줘야 하기 때문에 reverse()를 사용한다. 

    메세지 정보와 더불어 result, page의 정보를 같이 넘겨준다. 

    import axios from "axios"
    import { CONVERSATION_DELETE_FAIL, CONVERSATION_DELETE_REQUEST, CONVERSATION_DELETE_SUCCESS, CONVERSATION_GET_FAIL, CONVERSATION_GET_REQUEST, CONVERSATION_GET_SUCCESS, MESSAGE_ADD_FAIL, MESSAGE_ADD_REQUEST, MESSAGE_ADD_SUCCESS, MESSAGE_DELETE_FAIL, MESSAGE_DELETE_REQUEST, MESSAGE_DELETE_SUCCESS, MESSAGE_GET_FAIL, MESSAGE_GET_MORE_FAIL, MESSAGE_GET_MORE_REQUEST, MESSAGE_GET_MORE_SUCCESS, MESSAGE_GET_REQUEST, MESSAGE_GET_SUCCESS } from "../_constants/messageConstants"
    
    
    export const addMessage = ({msg})=>async(dispatch, getState)=>{
    
        const {userLogin:{userInfo}}  = getState()
        const {socket} = getState()
        dispatch({
            type:MESSAGE_ADD_REQUEST
        })
    
        try{
            let imgArr = [];
            if(msg.media.length>0){
                for(const item of msg.media){
                    const bodyFormData = new FormData()
                    if(item.camera){
                        bodyFormData.append("file", item.camera)
                    }else{
                        bodyFormData.append("file", item)
                    }
                    const data = await axios.post('/api/messageuploads', bodyFormData,{
                        headers:{'Content-Type' : 'multipart/form-data', authorization:`Bearer ${userInfo.token}`}
                    })
                    imgArr.push(data)
                }
            }
    
            const res = await axios.post('/api/message', {recipient:msg.recipient, text:msg.text, media:imgArr}, {
                headers:{authorization:`Bearer ${userInfo.token}`}
            })
    
            const {_id, avatar, fullname, username} = userInfo.user
    
            socket.emit('addMessage', {...msg, media:imgArr, user:{_id, avatar, fullname, username}})
    
            dispatch({
                type:MESSAGE_ADD_SUCCESS,
                payload:{...msg, media:imgArr}
            })
        }catch(error){
            dispatch({
                type:MESSAGE_ADD_FAIL,
                payload:                
                error.response && error.response.data.message
                ? error.response.data.message
                : error.message
            })
        }
    }
    
    
    export const getMessage = ({id})=>async(dispatch, getState)=>{
    
        const {userLogin:{userInfo}} = getState()
    
        const page = 1
    
        dispatch({
            type:MESSAGE_GET_REQUEST
        })
    
        try{
            const res = await axios.get(`/api/message/${id}`, {
                headers:{authorization:`Bearer ${userInfo.token}`}
            })
    
            const newData = {...res.data, messages:res.data.messages.reverse()}
    
            dispatch({
                type:MESSAGE_GET_SUCCESS,
                payload:{
                    ...newData,
                    _id:id,
                    page
                }
            })
    
        }catch(error){
            dispatch({
                type:MESSAGE_GET_FAIL,
                payload:                
                error.response && error.response.data.message
                ? error.response.data.message
                : error.message
            })
        }    
    }
    
    export const loadMoreMessages = ({id, page, limit})=>async(dispatch, getState)=>{
    
        const {userLogin:{userInfo}} = getState()
        dispatch({
            type:MESSAGE_GET_MORE_REQUEST
        })
    
        try{
            const res = await axios.get(`/api/message/${id}?page=${page}&limit=${limit}`, {
                headers:{authorization:`Bearer ${userInfo.token}`}
            })
    
            const newData = {...res.data, messages:res.data.messages.reverse()}
    
            dispatch({
                type:MESSAGE_GET_MORE_SUCCESS,
                payload:{
                    ...newData,
                    _id:id,
                    page
                }
            })
    
        }catch(error){
            dispatch({
                type:MESSAGE_GET_MORE_FAIL,
                payload:                
                error.response && error.response.data.message
                ? error.response.data.message
                : error.message
            })
        }   
    }
    
    export const deleteMessage = ({msg, data})=>async(dispatch, getState)=>{
    
        const newMessages = data.filter(item=>item._id !== msg._id)
    
        const {userLogin: {userInfo}} = getState()
        dispatch({
            type:MESSAGE_DELETE_REQUEST
        })
    
        try{
    
            await axios.delete(`/api/message/${msg._id}`, {
                headers:{authorization:`Bearer ${userInfo.token}`}
            })
    
            dispatch({
                type:MESSAGE_DELETE_SUCCESS,
                payload:{
                    newMessages,
                    _id:msg.recipient
                }
            })
    
        }catch(error){
            dispatch({
                type:MESSAGE_DELETE_FAIL,
                payload:                
                error.response && error.response.data.message
                ? error.response.data.message
                : error.message
            })
        }   
    
    }
    
    export const deleteConversation = ({id})=>async(dispatch, getState)=>{
    
        const {userLogin:{userInfo}} = getState()
        
        dispatch({
            type:CONVERSATION_DELETE_REQUEST
        })
    
        try{
    
            await axios.delete(`/api/message/conversation/${id}`,{
                headers:{authorization:`Bearer ${userInfo.token}`}
            })
    
            dispatch({
                type:CONVERSATION_DELETE_SUCCESS,
                payload:id
            })
    
        }catch(error){
            dispatch({
                type:CONVERSATION_DELETE_FAIL,
                payload:                
                error.response && error.response.data.message
                ? error.response.data.message
                : error.message
            })
        }   
    }

     

    메세지의 미디어를 저장하는 폴더를 다음과 같이 만들어준다. 

    (최상위 폴더에 messageuploads폴더 만들고, file.txt파일을 하나 만들어준다. )

    messageUploadRouter를 만들어준다. 

    const multer = require('multer')
    const express = require('express');
    const { auth } = require('../middleware/auth');
    
    const messageUploadRouter = express.Router();
    
    const storage = multer.diskStorage({
        destination(req,file,cb){
            cb(null, 'messageuploads/')
        },
        filename(req,file,cb){
            cb(null, `${Date.now()}_${file.originalname}`);
    
        }
    })
    
    const messageupload = multer({storage})
    
    messageUploadRouter.post('/',auth, messageupload.single('file'), (req,res)=>{
        res.send(`/${req.file.path}`)
    })
    
    module.exports = messageUploadRouter

     

    server에 업로드 라우터 추가해준다. 

    app.use('/messageuploads', express.static(path.join(__variableOfChoice, '/messageuploads')))
    
    
    app.use('/api/messageuploads', require('./routes/messageUploadRouter'));

     

    이제 reducer를 작성해준다. 

    state은 다음과 같이 구성된다. 

    users user 정보, online정보
    messages result(총메세지 개수), message정보들, 메세지창 해당 유저의 _id
    firstLoad 유저가 처음 로딩되는지, 
    loading 로딩상태인지

    메세지 는 배열로 이루어져있으며, 메세지 하나당 해당정보를 가진다. 

    만약 새로운 대화방을 누르면 다음과 같이 data-message정보가 추가된다. 

    import { CHECK_ONLINE, CONVERSATION_DELETE_REQUEST, CONVERSATION_DELETE_SUCCESS, CONVERSATION_GET_FAIL, CONVERSATION_GET_REQUEST, CONVERSATION_GET_SUCCESS, MESSAGE_ADD_FAIL, MESSAGE_ADD_REQUEST, MESSAGE_ADD_SUCCESS, MESSAGE_ADD_USER, MESSAGE_DELETE_FAIL, MESSAGE_DELETE_REQUEST, MESSAGE_DELETE_SUCCESS, MESSAGE_GET_FAIL, MESSAGE_GET_MORE_FAIL, MESSAGE_GET_MORE_REQUEST, MESSAGE_GET_MORE_SUCCESS, MESSAGE_GET_REQUEST, MESSAGE_GET_SUCCESS } from "../_constants/messageConstants";
    
    export const messageReducer = (state={ users:[], data:[]}, action)=>{
        switch(action.type){
    
    
            case MESSAGE_ADD_REQUEST:
                return {...state, loading:true}
            case MESSAGE_ADD_SUCCESS:
                return {...state, 
                        loading:false, 
                        data:state.data.map(item=> 
                            item._id === action.payload.recipient || item._id ===action.payload.sender
                            ? {
                                ...item,
                                messages:[...item.messages, action.payload],
                                result:item.result + 1
                            }
                            : item
                        ),
                        users:state.users.map(user=>
                                                user._id ===action.payload.recipient || user._id ===action.payload.sender
                                                ?{...user, text:action.payload.text, media:action.payload.media}     
                                                : user
                                            )}
                                            
            
            case MESSAGE_ADD_FAIL:
                return {...state, loading:false,  error:action.payload}
    
            case MESSAGE_GET_REQUEST:
                return {...state, loading:true}
            case MESSAGE_GET_SUCCESS:
                return {...state, loading:false, data:[...state.data, action.payload]}
            case MESSAGE_GET_FAIL:
                return {...state, loading:false, error:action.payload}
    
            case MESSAGE_GET_MORE_REQUEST:
                return {...state, loading:true}
            case MESSAGE_GET_MORE_SUCCESS:
                return {...state, loading:false, data:state.data.map(item=>item._id === action.payload._id? action.payload: item)}
            case MESSAGE_GET_MORE_FAIL:
                return {...state, loading:false, error:action.payload}
    
            case MESSAGE_DELETE_REQUEST:
                return {...state, loading:true}
            case MESSAGE_DELETE_SUCCESS:
                return {...state, 
                        data:state.data.map(item=>item._id===action.payload._id
                                        ? {...item, messages:action.payload.newMessages}
                                        : item)}
            case MESSAGE_DELETE_FAIL:
                return {...state, error:action.payload}
    
            case CONVERSATION_DELETE_REQUEST:
                return {...state, loading:true}
            case CONVERSATION_DELETE_SUCCESS:
                return {...state, 
                        data: state.data.filter(item=>item._id !== action.payload),
                        users: state.users.filter(item=>item._id !== action.payload)}
    
            default:
                return state
        }
    }

     

     

     

     

    이제 모든 정보는 가져왔으니 

    rigth side 바를 구성하자. 

     

    맨처음 로딩되었을때, 

    이미 데이터에 해당 message/:id(대화나눈상대아이디)와의 대화 정보가 있으면, 

    data(메세지), result, page를 셋팅한다. 

     

    대화방 정보가 있으면, (대화방 리스트가 여러명이면) 스크롤을 제일 아래로 내리고, 

    아이디에 맞는 message.users정보에 있는 사용자를 user로 셋팅한다. 

     

    데이터에 해당 아이디의 정보가 없으면 새로 데이터를 받아오고 데이터를 셋팅해준다. 

    useRef를 이용해서 해당 링크를 걸어놓고 (chat_container, chat_display)

     

    intersectionobser를 이용해서 옵저버 위치에서 intersection하면 loadmore을 발생시킨다. 

     

    input해서 새로운 메세지를 입력하는데, 이미지도 선택가능하도록 설정해주고, 

    이미지 타입체크도하고, 

    새로운 메세지를 보낼때는 useRef를 이용해서 스크롤이 가장 아래로 내려오도록 설정했다. 

    import React, { useEffect, useState, useRef } from 'react'
    import { useDispatch, useSelector } from 'react-redux'
    import { addMessage, deleteConversation, getMessage, loadMoreMessages } from '../../_actions/messageActions'
    import { ALERT } from '../../_constants/globalConstants'
    import Icons from '../Icons'
    import UserCard from '../UserCard'
    import Display from './Display'
    import LoadIcon from '../../images/loading.gif'
    import { useHistory } from 'react-router-dom'
    
    function RightSide({id}) {
    
    
    
        const userLogin = useSelector(state => state.userLogin)
        const {userInfo} = userLogin
    
        const message = useSelector(state => state.message)
    
        const [user, setUser] = useState([])
        const [text, setText] = useState("")
        const [media, setMedia] = useState([])
        const [load, setLoad] = useState(false)
    
        const [data, setData] = useState([])
        const [result, setResult] = useState(9)
        const [page, setPage] = useState(0)
        const [isLoadMore, setIsLoadMore] = useState(false)
        const [limit, setLimit] = useState(9)
    
        const [preScrollHeight, setPreScrollHeight] = useState(0)
    
        const dispatch = useDispatch()
        const history = useHistory()
    
        const ref = useRef()
        const pageEnd = useRef()
    
        useEffect(() => {
            const newData = message.data.find(item=>item._id ===id)
            if(newData){
                setData(newData.messages)
                setResult(newData.result)
                setPage(newData.page)
            }
        }, [message.data, id])
    
    
        useEffect(() => {
            if(id && message.users.length>0){
                setTimeout(()=>{
                    ref.current.scrollIntoView({behavior:'smooth', block:'end'})
                },50)
                const newUser = message.users.find(item=>item._id===id)
                if(newUser){
                    setUser(newUser)
                }
    
            }
        }, [dispatch, id, message.users])
    
    
        useEffect(() => {
            const getMessageData = async()=>{
                const existingUser = message.data.filter(item=> item._id ===id)
                if(existingUser.length ===0){
                    await dispatch(getMessage({id}))
                    setTimeout(()=>{
                        ref.current.scrollIntoView({behavior:'smooth', block:'end'})
                    },50)
                }
            }
            getMessageData()
        }, [id, dispatch, message.data])
    
    
        useEffect(() => {
            const observer = new IntersectionObserver(entries=>{
                if(entries[0].isIntersecting){
                    setIsLoadMore(true)
                }
            },{
                threshold:0.1
            })
            observer.observe(pageEnd.current)
        }, [])
    
        useEffect(() => {
            if(isLoadMore){
                if(result > limit || result===limit){
                    setPage(page=>page+1)
                    dispatch(loadMoreMessages({id, page:page+1, limit}))
                }
                setIsLoadMore(false)      
            }
        }, [isLoadMore])
    
        // const handleScroll = (preScrollHeight)=>{
        //     ref.current.scrollTop= ref.current.scrollHeight - preScrollHeight
        // }
    
    
        const handleChangeMedia=(e) =>{
    
            const files = [...e.target.files]
            let newMedia= []
            let err = ''
    
            files.forEach(file=>{
                if(!file) return err = "File doesn't exist."
                // if(file.size >1024*1024*5){
                //     return err = "The image largest is 5mb."
                // }
                if(file.type !== 'image/jpeg' && file.type !=='image/png' && file.type !== 'video/mp4' && file.type !== 'video/avi' && file.type !== 'video/wmv' && file.type !=='video/mov'){
                    return err = "File format is incorrect."
                }
                return newMedia.push(file)
            })
    
            if(err) dispatch({type:ALERT, payload:{error:err}})
    
            setMedia([...media, ...newMedia])
        }
    
        const imageShow=(src) =>{
            return (
                <img src={src} alt="images" className="img-thumbnail" />
            )
        }
    
        const videoShow=(src) =>{
            return (
                <video src={src} alt="images" className="img-thumbnail" />
            )
        }
        
        const handleDeleteMedia= (index)=>{
            const newArr=[...media]
            newArr.splice(index, 1)
            setMedia(newArr)
        }
    
        const handleSubmit= async(e)=>{
            e.preventDefault()
    
            if(!text.trim() && media.length===0)return ;
    
            setLoad(true)
            
            const msg={
                sender: userInfo.user._id,
                recipient:id,
                text,
                media,
                createdAt:new Date().toISOString()
            }
    
    
            await dispatch(addMessage({msg}))
            
            setText('')
            setMedia([])
            setLoad(false)
    
            if(ref.current){
                setTimeout(()=>{
                    ref.current.scrollIntoView({behavior:'smooth', block:'end'})
                },50)
            }
        }
        
        const handleDeleteConversation = ()=>{
            if(window.confirm('Are you sure to leave this chat room?')){
                dispatch(deleteConversation({id}))
                return history.push('/message')
            }
        }
    
    
        return (
            <>
                <div className="message_header" style={{cursor:'pointer'}}>
                    {
                        user.length !== 0 &&
                        <UserCard user={user} id = {id}>
                            <i className="fas fa-trash text-danger" onClick={handleDeleteConversation}></i>
                        </UserCard>
                    }
                </div>
                <div className="chat_container" style={{height: media.length >0? 'calc(100%-180px)':''}} >
                    <div className="chat_display" ref={ref}  >
                        <button style={{marginTop:'-25px', opacity:0}} ref={pageEnd}>
                            Loadmore
                        </button>
                        {
                            data.map((msg, index)=>(
                                <div key={index}>
                                    {
                                        msg.sender !== userInfo.user._id &&
                                        <div className="chat_row other_message">
                                            <Display user={user} msg={msg}/>
                                        </div>
                                    }
                                    
                                    {
                                        msg.sender === userInfo.user._id &&
                                        <div className="chat_row you_message">
                                            <Display user={userInfo.user} msg={msg} data={data}/>
                                        </div>
                                    }
    
                                </div>
                            ))
                        }
    
                    </div>
                </div>
                {
                    load &&
                    <div className="chat_row you_message">
                        <img src={LoadIcon} alt="loading" />
                    </div>
                }
    
                <div className="show_media" style={{display: media.length>0? 'grid':'none'}}>
                    {
                        media && 
                        media.map((item, index)=>(
                            <div key={index} id="file_media">
                                {
                                    item.type.match(/video/i)
                                    ? videoShow(URL.createObjectURL(item))
                                    : imageShow(URL.createObjectURL(item))
                                }
                                <span onClick={()=> handleDeleteMedia(index)}>&times;</span>
                            </div>
                        ))
                    }
                </div>
    
                <form className="chat_input" onSubmit={handleSubmit}>
                    <input type="text" placeholder="Enter your Message." value={text} onChange={(e)=>setText(e.target.value)}/>
    
                    <Icons setContent={setText} content={text}/>
    
                    <div className="file_upload">
                        <i className="fas fa-image text-danger"></i>
                        <input type="file" name="file" id ="file" multiple accept="image/*, video/*" onChange={handleChangeMedia}/>
                    </div>
    
                    <button type="submit" className="material-icons" disabled={text || media.length> 0? false : true}>
                        near_me
                    </button>
                </form>
            </>
        )
    }
    
    export default RightSide

    메세지를 보여주는 display컴포넌트를 만들어준다. 

     

    import React from 'react'
    import { useDispatch, useSelector } from 'react-redux'
    import { deleteMessage } from '../../_actions/messageActions'
    import Avatar from '../Avatar'
    
    function Display({user, msg, data}) {
    
        const userLogin = useSelector(state => state.userLogin)
        const {userInfo} = userLogin
    
        const dispatch = useDispatch()
    
        const imageShow=(src) =>{
            return (
                <img src={src} alt="images" className="img-thumbnail" />
            )
        }
    
        const videoShow=(src) =>{
            return (
                <video src={src} alt="images" className="img-thumbnail" />
            )
        }
        const handleDeleteMessage=()=>{
            if(!data) return ;
            if(window.confirm('Do you want to delete this message?')){
                dispatch(deleteMessage({msg, data}))
            }
        }
        return (
           <>
           <div className="chat_title">
                <Avatar src={user.avatar} size="small-avatar"/>
                <span>{user.username}</span>
           </div>
    
            <div className="you_content">
                {
                    user._id === userInfo.user._id &&
                    <i className="fas fa-trash text-danger" onClick={handleDeleteMessage}></i>
    
                }
                <div>
                    {
                        msg.text && 
                        <div className="chat_text">
                            {msg.text}
                        </div>
    
                    }
    
                    {
                        msg.media && 
                        msg.media.map((item, index)=>(
                            <div key={index}>
                                {
                                    item.data.match(/video/i)||item.data.match(/mp4/i)||item.data.match(/avi/i)||item.data.match(/mov/i)||item.data.match(/wmv/i)
                                    ? videoShow(item.data)
                                    : imageShow(item.data)
                                }
                            </div>
                        ))
                    }
                </div>
            </div>
    
           <div className="chat_time">
               {new Date(msg.createdAt).toLocaleString()}
           </div>
           </>
        )
    }
    
    export default Display

     

    실시간으로 정보를 주고받기 위해서 다음과같이 설정해준다. 

     

    socket.server

        //Message
        socket.on('addMessage', msg=>{
            const user = users.find(user=>msg.recipient === user.id)
            user && socket.to(`${user.socketId}`).emit('addMessageToClient', msg)
        })

     

     

    socket.client

    유저목록에 없으면 추가해줘야 한다. 

        //Message
        useEffect(() => {
            socket.on('addMessageToClient', msg=>{
                dispatch({
                    type:MESSAGE_ADD_SUCCESS,
                    payload:msg
                })
    
                dispatch({
                    type:MESSAGE_ADD_USER,
                    payload:{
                        ...msg.user,
                        text:msg.text,
                        media:msg.media
                    }
                })
            })
            return () => socket.off('addMessageToClient')
        }, [socket, dispatch])

     

     

     

    아이콘도 입력해주기 위해서 아이콘 컴포넌트를 따로 만들어준다. 

    드롭다운 모양으로 내려오도록 한다. 

     

    import React from 'react'
    
    function Icons({setContent, content}) {
        const reactions = [   
            '❤️', '😆', '😯', '😢', '😡', '👍', '👎', '😄',
            '😂', '😍', '😘', '😗', '😚', '😳', '😭', '😓',
            '😤', '🤤', '👻', '💀', '🤐', '😴', '😷', '😵',
            '😖', '😣', '😔', '😵‍💫'
        ]
        return (
           <div className="nav-item dropdown" style={{opacity:1}}>
                <span className="nav-link position-relative px-1" id="navbarDropdown"
                role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <span style={{opacity:0.4}}>😄</span>
                </span>
    
                <div className="dropdown-menu" aria-labelledby="navbarDropdown">
                    <div className="reactions">
                        {
                            reactions.map(icon=>(
                                <span key={icon} onClick={(e)=>setContent(content+icon)}>
                                    {icon}
                                </span>
                            ))
                        }
                    </div>
                </div>
            </div>
        )
    }
    
    export default Icons

     

    댓글창 입력할때도, 

    포스트 입력할때도 아이콘 입력할 수 있도록 해준다. 

    inputComment.js

    
    
        return (
    
                    <div className="flex-fill"></div>
                    <Icons setContent={setContent} content={content} />
                </div>
    
    
    
    export default InputComment

     

    statusModal.js

                 <div className="d-flex">
                            <div className="flex-fill"></div>
                            <Icons setContent={setcontent} content={content} />
                        </div>

    icon.css

    .reactions{
        display: grid;
        grid-template-columns: repeat(4, 50px);
        text-align: center;
        cursor: pointer;
    }
    .reactions span{
        margin: 3px 0;
    }

     

     

     

    추가로 지금 동영상올리는 것이 안되는데, 이것이 가능하도록 설정하자. 

    postUploadRouter

    포스트 파일이름 저장을 다음과 같이 originalname으로 설정해준다. (.mp4, .avi등의 형식)

    const multer = require('multer')
    const express = require('express');
    const { auth } = require('../middleware/auth');
    
    const postUploadRouter = express.Router();
    
    const storage = multer.diskStorage({
        destination(req,file,cb){
            cb(null, 'postuploads/')
        },
        filename(req,file,cb){
            cb(null, `${Date.now()}_${file.originalname}`);
    
        }
    })
    
    const postupload = multer({storage})
    
    postUploadRouter.post('/',auth, postupload.single('file'), (req,res)=>{
        res.send(`/${req.file.path}`)
    })
    
    module.exports = postUploadRouter

     

     

    carousel을 다음과 같이 수정해준다. 

                <div className="carousel-inner">
                    {
                        images.map((img, index) => (
                            <div key={index} className={`carousel-item ${isActive(index)}`}>
                                {
                                    img.data.match(/video/i)||img.data.match(/mp4/i)||img.data.match(/avi/i)||img.data.match(/mov/i)||img.data.match(/wmv/i)
                                    ? <video controls src={img.data} className="d-block w-100" alt={img.data}
                                    style={{filter: theme ? 'invert(1)' : 'invert(0)'}} />
    
                                    : <img src={img.data} className="d-block w-100" alt={img.data}
                                    style={{filter: theme ? 'invert(1)' : 'invert(0)'}} />
                                }

     

     

     

     

    postThumb도 수정해준다. 

                            <div className="post_thumb_display">
                                {
                                    post.images[0]?.data.match(/video/i)||post.images[0]?.data.match(/mp4/i)||post.images[0]?.data.match(/avi/i)||post.images[0]?.data.match(/mov/i)||post.images[0]?.data.match(/wmv/i)
                                    ? <video controls src={post.images[0]?.data} alt={post.images[0]?.data}></video>
                                    : <img src={post.images[0]?.data} alt={post.images[0]?.data}></img>
                                }

     

    profile도 설정

                    {
                                posts?.map(post=>(
                                    <Link key={post._id} to={`/post/${post._id}`}>
                                        <div className="post_thumb_display">
                                            {
                                                post.images[0]?.data.match(/video/i)||post.images[0]?.data.match(/mp4/i)||post.images[0]?.data.match(/avi/i)||post.images[0]?.data.match(/mov/i)||post.images[0]?.data.match(/wmv/i)
                                                ? <video controls src={post.images[0].data} alt={post.images[0].data}></video>
                                                : <img src={post.images[0].data} alt={post.images[0].data}></img>
                                            }

     

     

     

     

     

     

     

Designed by Tistory.