2024.05.28 채팅 UI & 팔로우 UI 코드 - FlowNary SNS 29일차

2024. 5. 28. 17:03React

작업 전 첫 팔로우 화면

팔로우 & 팔로워 UI 작업 전

 

중간 수정 단계 1

중간 수정 단계 1 - 나

 

 

중간 수정 단계 2

중간 수정 단계 2 - 영준

 

중간 수정 단계 3 - 영준

 

중간 수정 단계 4 - 영준

 

중간 수정 단계 5 - 영준

 

중간 수정 단계 6 - 영준

 

 

chatting < ChattingIndex.js

import React, { useState, useRef, useEffect, useContext } from 'react';
import { Avatar, Box, Stack, TextField, InputAdornment, Icon } from "@mui/material";
import IconButton from '@mui/material/IconButton';
import EastIcon from '@mui/icons-material/East';
import { GetWithExpiry } from 'api/LocalStorage';
import './components/chat.css';
import DashboardLayout from 'examples/LayoutContainers/DashboardLayout';
import DashboardNavbar from 'examples/Navbars/DashboardNavbar';
import { UserContext } from 'api/LocalStorage';
import { useLocation } from 'react-router-dom';
import { useWebSocket } from 'api/webSocketContext';
import { getDmList } from 'api/axiosGet';
import { getChat } from 'api/axiosGet';
import TouchRipple from '@mui/material/ButtonBase/TouchRipple';

export default function Chat() {
    const [messages, setMessages] = useState([]);
    const [inputMessage, setInputMessage] = useState('');
    const messageEndRef = useRef(null);
    const inputFieldHeight = 65; // 입력 필드의 높이를 고정값
    const { activeUser } = useContext(UserContext);
    const { state } = useLocation() || null;
    const { cid } = state != null ? state : { cid: -1 };
    const { stompClient } = useWebSocket();
    const [count, setCount] = useState(20);
    const [list, setList] = useState([]);
    const [chatroom, setChatroom] = useState(null);
    const profile = GetWithExpiry("profile");

    const handleMessageSend = () => {
        if (inputMessage.trim() !== '' && stompClient && stompClient.connected) {
            const newMessage = { text: inputMessage, sender: 'user' };
            setInputMessage('');

            stompClient.publish({
                destination: '/app/chatroom',
                body: JSON.stringify({
                    cid: cid,
                    uid: activeUser.uid,
                    dContents: inputMessage,
                    dFile: null,
                    nickname: activeUser.nickname,
                    profile: profile,
                    status: chatroom.status,
                }),
            })
        }
    };

    const handleKeyPress = (event) => {
        if (event.key === 'Enter') {
            // event.preventDefault(); // 기본 엔터 기능 비활성화
            handleMessageSend(); // 메시지 전송 함수 호출
        }
    };


    useEffect(() => {
        if (activeUser.uid !== -1 && cid !== -1) {
            const fetchMList = async () => {
                const mlist = await getDmList(cid, count);
                setMessages(mlist);
                const chatr = await getChat(cid, activeUser.uid);
                setChatroom(chatr);
            }

            fetchMList();
            let chatconnect;

            if (stompClient && stompClient.connected) {
                console.log('chatting room connected');
                stompClient.publish({
                    destination: '/app/page',
                    body: JSON.stringify({ userId: activeUser.uid, page: 'chatroom' + cid, action: 'enter' }),
                });

                chatconnect = stompClient.subscribe(`/user/chat/` + cid, (message) => {
                    const data = JSON.parse(message.body);
                    // console.log(data);
                    setMessages(prevMessages => {
                        const messageExists = prevMessages.some(msg => msg.did === data.did);
                        if (!messageExists) {
                            return [data, ...prevMessages];
                        }
                        return prevMessages;
                    });
                });
            }

            return () => {
                if (stompClient && stompClient.connected) {
                    stompClient.publish({
                        destination: '/app/page',
                        body: JSON.stringify({ userId: activeUser.uid, page: 'chatroom' + cid, action: 'leave' }),
                    });
                    console.log('chatting room disconnected');
                }
            }
        }
    }, [activeUser.uid, stompClient]);

    useEffect(() => {
        if (messageEndRef.current) {
            messageEndRef.current.scrollIntoView({ behavior: "smooth", block: "end" });
        }
    }, [messages]);

    const rippleRef = useRef(null);

    const handleMouseDown = (event) => {
        rippleRef.current.start(event);
    };

    const handleMouseUp = () => {
        rippleRef.current.stop();
    };

    if (cid === -1) {
        return (
            <div>채팅방 정보가 없습니다.</div>
        )
    }

    return (
        <DashboardLayout>
            <DashboardNavbar />
            <IconButton sx={{ fontSize: '3rem', cursor: 'pointer' }}
                onMouseDown={handleMouseDown}
                onMouseUp={handleMouseUp}>
                <Icon>arrow_back</Icon>
                <TouchRipple ref={rippleRef} center />
            </IconButton>
            <Box
                sx={{
                    marginTop: '-70px',
                    padding: '0px 15px 10px 10px',
                    minHeight: '200px',
                    // height: 'calc(180vh - 200px)',
                    width: '80%',
                    mx: 'auto',
                    overflowY: 'auto',
                }}
            >
                <Stack sx={{ fontSize: 'x-large', fontWeight: 'bold', mx: 'auto' }}>
                    <div style={{ color: 'rgb(88, 67, 135)' }}>
                        {/* <Avatar alt="User" src={`https://res.cloudinary.com/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload/${profile}`} />
                        {email} */}
                        {chatroom && chatroom.name}
                        <hr style={{ opacity: '0.4' }} />
                    </div>
                </Stack>
                {/* maxHeight를 사용 스크롤 활성 */}
                <Stack sx={{ maxHeight: `calc(100vh - ${inputFieldHeight + 385}px)`, overflowY: 'auto', flexDirection: 'column-reverse' }}> {/* 메시지 영역의 최대 높이를 조정 */}
                    <br />
                    <div ref={messageEndRef} />
                    {messages && messages.map((message, index) => (
                        <Stack
                            key={index}
                            direction='row'
                            justifyContent={message.uid === activeUser.uid ? 'flex-end' : 'flex-start'}
                        >
                            {message.uid !== activeUser.uid &&
                                <Avatar sx={{ width: 50, height: 50 }}

                                >R</Avatar>}
                            <div style={{ marginTop: '20px' }} className={message.uid === activeUser.uid ? "message" : "othermessage"}>{message.dContents}</div>
                            {message.uid === activeUser.uid &&
                                <Avatar style={{ marginTop: '10px', marginLeft: '5px', marginBottom: '-10px' }}
                                    sx={{ width: 50, height: 50, marginRight: '.75rem' }}
                                    src={`https://res.cloudinary.com/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload/${message.profile}`}

                                >U</Avatar>}
                        </Stack>
                    ))}
                </Stack>
                <Stack
                    sx={{
                        position: 'fixed',
                        bottom: '10px',
                        width: { xs: '60%', sm: '70%', md: '80%' },
                    }}
                >
                    <TextField //원래 이랬음
                        sx={{
                            marginBottom: '4em',
                            height: `${inputFieldHeight}px`, // 입력 필드의 높이 설정
                            width: '70.5%',
                        }}
                        fullWidth
                        placeholder="메시지를 보내세요!"
                        variant="outlined"
                        value={inputMessage}
                        onChange={(e) => setInputMessage(e.target.value)}
                        onKeyPress={handleKeyPress}
                        InputProps={{
                            endAdornment: (
                                <InputAdornment position="end" >
                                    <IconButton onClick={handleMessageSend}>
                                        <EastIcon />
                                    </IconButton>
                                </InputAdornment>
                            ),
                        }}
                    />
                    {/* {{chatGPT에게 물어본 코드}}
                    <TextField
                        sx={{
                            marginBottom: '4em',
                            height: `${inputFieldHeight}px`,
                            width: '70.5%',
                        }}
                        fullWidth
                        placeholder="메시지를 보내세요!"
                        variant="outlined"
                        value={inputMessage}
                        onChange={(e) => setInputMessage(e.target.value)}
                        onKeyDown={(e) => {
                            if (e.key === 'Enter') {
                                // e.preventDefault(); // 기본 엔터 기능 비활성화
                                handleMessageSend(); // 메시지 전송 함수 호출
                            }
                        }}
                        InputProps={{
                            endAdornment: (
                                <InputAdornment position="end">
                                    <IconButton onClick={handleMessageSend}>
                                        <EastIcon />
                                    </IconButton>
                                </InputAdornment>
                            ),
                        }}
                    /> */}
                </Stack>
            </Box>
        </DashboardLayout>
    );
}

 

 

chatting < Chattinglist.js.js

import { ListItem, List, ListItemAvatar, Avatar } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { UserContext } from 'api/LocalStorage';
import { getChatList } from 'api/axiosGet';
import { useWebSocket } from 'api/webSocketContext';
import DashboardLayout from 'examples/LayoutContainers/DashboardLayout';
import DashboardNavbar from 'examples/Navbars/DashboardNavbar';
import React, { useState, useEffect, useContext } from 'react';
import { useNavigate } from 'react-router-dom';

export default function ChatList() {
    const { activeUser } = useContext(UserContext);
    const [list, setList] = useState([]);
    const { stompClient } = useWebSocket();
    const [count, setCount] = useState(20);
    const navigate = useNavigate();

    useEffect(() => {
        if (activeUser.uid !== -1) {
            const fetchChatList = async () => {
                const chatlist = await getChatList(activeUser.uid, count, 0);
                console.log(chatlist);
                setList(chatlist);
            }

            fetchChatList();
            let chatrefresh;

            if (stompClient && stompClient.connected) {
                console.log('chat websocket connected');
                stompClient.publish({
                    destination: '/app/page',
                    body: JSON.stringify({ userId: activeUser.uid, page: 'chat', action: 'enter' }),
                });

                chatrefresh = stompClient.subscribe(`/topic/chatlist`, (message) => {
                    const data = JSON.parse(message.body);
                    console.log(data);

                    setList(prevList => {
                        const indexcid = prevList.findIndex(item => item.cid === data.cid);
                        // console.log(list);
                        console.log(indexcid);
                        if (indexcid !== -1) {
                            const chat = prevList[indexcid];
                            const newlist = [{
                                cid: data.cid,
                                status: chat.status,
                                statusTime: chat.statusTime,
                                userCount: chat.userCount,
                                name: chat.name,
                                lastMessage: data.lastMessage,
                            }
                                , ...prevList.slice(0, indexcid), ...prevList.slice(indexcid + 1)];
                            console.log(newlist);

                            return newlist;
                        }
                        return prevList;
                    })
                });
            }

            return () => {
                if (stompClient && stompClient.connected) {
                    stompClient.publish({
                        destination: '/app/page',
                        body: JSON.stringify({ userId: activeUser.uid, page: 'chat', action: 'leave' }),
                    });
                    console.log('chat websocket disconnected');
                }

                if (chatrefresh) {
                    chatrefresh.unsubscribe();
                }
            }
        }
    }, [activeUser.uid, count, stompClient]);

    const handleChatClick = (cid) => {
        navigate("/chatting", { state: { cid: cid } });
    }
    
    function formatDate(dateString) {
        const date = new Date(dateString);

        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');

        return `${year}년 ${month}월 ${day}일 ${hours}:${minutes}`;
    }

    return (
        <DashboardLayout>
            <DashboardNavbar />
            <List>
                {list && list.map((data, idx) => (
                    <ListItem key={idx} onClick={() => handleChatClick(data.cid)}>
                        <ListItemAvatar>
                            <Avatar
                                sx={{ bgcolor: 'red'[500] }}
                                aria-label="recipe"
                                src={`https://res.cloudinary.com/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload/Cloudinary-React/dddyq4tkk7gorhfm3twm`}
                            />
                        </ListItemAvatar>
                        {/* {data.cid} <br /> */}
                        {data.name} <br />
                        {data.lastMessage} <br />
                        {formatDate(data.statusTime)} <br />
                    </ListItem>
                ))}
            </List>
        </DashboardLayout>
    )
}

 

 

chatting < Chattingtemp.js

import React, { useState, useRef, useEffect, useContext } from 'react';
import { Avatar, Box, Stack, TextField, InputAdornment } from "@mui/material";
import IconButton from '@mui/material/IconButton';
import EastIcon from '@mui/icons-material/East';
import { GetWithExpiry } from 'api/LocalStorage';
import './components/chat.css';
import DashboardLayout from 'examples/LayoutContainers/DashboardLayout';
import DashboardNavbar from 'examples/Navbars/DashboardNavbar';
import { UserContext } from 'api/LocalStorage';
import { useLocation, useNavigate } from 'react-router-dom';
import { useWebSocket } from 'api/webSocketContext';
import { getDmList } from 'api/axiosGet';
import { getChat } from 'api/axiosGet';
import { getUserNickEmail } from 'api/axiosGet';
import { insertChat } from 'api/axiosPost';

export default function ChatTemp() {
    const navigate = useNavigate();
    const [messages, setMessages] = useState([]);
    const [inputMessage, setInputMessage] = useState('');
    const messageEndRef = useRef(null);
    const inputFieldHeight = 65; // 입력 필드의 높이를 고정값
    const { activeUser } = useContext(UserContext);
    const { state } = useLocation() || {};
    const { uid1, uid2 } = state;
    const { stompClient } = useWebSocket();
    const [count, setCount] = useState(20);
    const [list, setList] = useState([]);
    const [chatroom, setChatroom] = useState(null);
    const profile = GetWithExpiry("profile");

    const [user, setUser] = useState(null);
    const [name, setName] = useState('');
    useEffect(() => {
        const user2 = getUserNickEmail(uid2);
        setUser(user2);
        setName(user2.nickname + ' 채팅방');
    }, [])

    const handleMessageSend = () => {
        if (inputMessage.trim() !== '' && stompClient && stompClient.connected) {
            const cid = insertChat(name, uid1, uid2, inputMessage );

            setInputMessage('');
            if (cid >= 0) {
                navigate("/chatting", {state: {cid: cid}});
            }
        }
    };

    const handleKeyPress = (event) => {
        if (event.key === 'Enter') {
            handleMessageSend();
        }
    };

    return (
        <DashboardLayout>
            <DashboardNavbar />
            <Box
                sx={{
                    top: '10%',
                    margin: '20px',
                    padding: '20px',
                    minHeight: '400px',
                    height: 'calc(100vh - 200px)',
                    width: '80%',
                    mx: 'auto',
                    overflowY: 'auto',
                }}
            >
                <Stack sx={{ fontSize: 'xx-large', fontWeight: 'bold', mx: 'auto' }}>
                    <div style={{ color: 'rgb(88, 67, 135)' }}>
                        {/* <Avatar alt="User" src={`https://res.cloudinary.com/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload/${profile}`} />
                        {email} */}
                        {name}와의 채팅방
                        <hr style={{ opacity: '0.4', marginTop: 20 }} />
                    </div>
                </Stack>
                {/* maxHeight를 사용 스크롤 활성 */}
                <Stack sx={{ maxHeight: `calc(100vh - ${inputFieldHeight + 385}px)`, overflowY: 'auto', flexDirection: 'column-reverse' }}> {/* 메시지 영역의 최대 높이를 조정 */}
                    <br />
                </Stack>
                <Stack
                    sx={{
                        position: 'fixed',
                        bottom: '5px',
                        width: { xs: '60%', sm: '70%', md: '80%' },
                    }}
                >
                    <TextField
                        sx={{
                            marginBottom: '1.5em',
                            height: `${inputFieldHeight}px`, // 입력 필드의 높이 설정
                            width: '70.5%',
                        }}
                        fullWidth
                        placeholder="메시지를 입력하세요..."
                        variant="outlined"
                        value={inputMessage}
                        onChange={(e) => setInputMessage(e.target.value)}
                        onKeyPress={handleKeyPress}
                        InputProps={{
                            endAdornment: (
                                <InputAdornment position="end" >
                                    <IconButton onClick={handleMessageSend}>
                                        <EastIcon />
                                    </IconButton>
                                </InputAdornment>
                            ),
                        }}
                    />
                </Stack>
            </Box>
        </DashboardLayout>
    );
}

 

 

chatting < components < chat.css

.message {
    font-size: 1.1rem;
    /* 글자 크기 설정*/
    background-color: rgb(153, 95, 180);
    /* 배경색 설정 */
    border-radius: 50px;
    /* 테두리를 둥글게 만듭니다. */
    padding: 0dvh 1dvh 0dvh 1.2dvh;
    /* 내용과 테두리 사이의 간격 설정 */
    margin: 0 0 10.5px 5px;
    /* 채팅 버블 간의 간격 설정 */
    max-width: 100%;
    /* 채팅 버블의 최대 너비 설정 */
    max-height: 60%;
    color: rgb(255, 255, 255);
}

.othermessage {
    font-size: 1.1rem;
    /* 글자 크기 설정*/
    background-color: rgb(191, 169, 219);
    /* 배경색 설정 */
    border-radius: 50px;
    /* 테두리를 둥글게 만듭니다. */
    padding: 0dvh 1.2dvh 0dvh 1dvh;
    /* 내용과 테두리 사이의 간격 설정 */
    margin: 0 0 10.5px 5px;
    /* 채팅 버블 간의 간격 설정 */
    max-width: 100%;
    /* 채팅 버블의 최대 너비 설정 */
    max-height: 50px;
    color: rgb(255, 255, 255);
}

 

 

chatting < components < ChattingModal.jsx

// 기본
import React, { useState } from 'react';
import { Box, Modal, Grid, ListItem, List, Divider, ListItemAvatar, Avatar, ListItemText, ListSubheader } from '@mui/material';

// 아이콘
import MessageIcon from '@mui/icons-material/Message';
import ClearIcon from '@mui/icons-material/Clear';
import ForumIcon from '@mui/icons-material/Forum';

// Components/css 연결 
import '../notice.css';
import Chat from './Chat';

export default function ChattingModal() {
  const [open, setOpen] = useState(false);
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  return (
    <>
      {/* Aside에 표시될 DM부분 표현 */}
      <button onClick={handleOpen} className='asideStyle'>
        <Grid container>
          <Grid item xs={12} lg={6} sx={{ display: { xs: 'flex', lg: 'flex' }, pl: 3 }}>
            <MessageIcon className='iconStyle' />
          </Grid>
          <Grid item xs={0} lg={6} sx={{ color:'rgb(58, 0, 85)', display: { xs: 'none', lg: 'flex' }, pr: 3, justifyContent: 'flex-end' }}>
            DM
          </Grid>
        </Grid>
      </button>

      {/* DM 부분 Modal */}
      <Modal open={open} onClose={handleClose} aria-labelledby="modal-modal-title" aria-describedby="modal-modal-description">
        <Box className='styleBox2'>

          {/* 메시지 목록 리스트 */}
          <List sx={{ width: '20%', boxShadow: '0 0 6px rgba(0, 0, 0, 0.5)', padding: 3, justifyContent: 'center', alignItems: 'center', }}
            subheader={
              <>
                <Grid item sx={{ display: { xs: 'none', lg: 'flex' }, justifyContent: 'center', alignItems: 'center', }} >
                  <ListSubheader component="div" id="nested-list-subheader" sx={{ fontSize: 25, }}>
                    메시지
                  </ListSubheader>
                </Grid>
                <Grid item sx={{ display: { xs: 'flex', lg: 'none' }, justifyContent: 'center', alignItems: 'center',  }} >
                  <ListSubheader component="div" id="nested-list-subheader" sx={{ textAlign: 'center', fontSize: 25 }}>
                    <ForumIcon />
                  </ListSubheader>
                </Grid>
              </>
            }>
            <ListItem alignItems="flex-start" sx={{ marginBottom: '10%' }} >
              <ListItemAvatar>
                <Avatar alt="Remy Sharp" src="/static/images/avatar/1.jpg" />
              </ListItemAvatar>
              <Grid item sx={{ display: { xs: 'none', lg: 'flex' } }} >
                <ListItemText
                  primary='곽주영'
                  secondary='2024-04-25'
                />
              </Grid>
            </ListItem>
            <Divider variant="inset" component="li" />
            <ListItem alignItems="flex-start" sx={{ marginBottom: '10%' }}>
              <ListItemAvatar>
                <Avatar alt="Remy Sharp" src="/static/images/avatar/1.jpg" />
              </ListItemAvatar>
              <Grid item sx={{ display: { xs: 'none', lg: 'flex' } }} >
                <ListItemText
                  primary='곽주영'
                  secondary='2024-04-25'
                />
              </Grid>
            </ListItem>
            <Divider variant="inset" component="li" />

            <ListItem alignItems="flex-start" sx={{ marginBottom: '10%' }}>
              <ListItemAvatar>
                <Avatar alt="Remy Sharp" src="/static/images/avatar/1.jpg" />
              </ListItemAvatar>
              <Grid item sx={{ display: { xs: 'none', lg: 'flex' } }} >
                <ListItemText
                  primary='곽주영'
                  secondary='2024-04-25'
                />
              </Grid>
            </ListItem>
            <Divider variant="inset" component="li" />
          </List>

          {/* Chatting을 구현한 Components 추가*/}
          <Chat />

          {/* 닫기 버튼 */}
          <div className='board_div_style_2'>
            <ClearIcon onClick={handleClose} sx={{ cursor: 'pointer', fontSize: '26px', backgroundColor: 'rgb(162, 152, 182)', borderRadius: '100%', margin: '3px' }} />
          </div>
        </Box>
      </Modal >
    </>
  );
}

 

 

 

채팅 UI - 초기 모습
채팅 UI - 중간 단계
채팅 UI 완성 - 나

오늘 수정하는 부분은 팔로우UI 와 채팅 UI 다!

오늘 반드시 끝내고 간다!!

내일은 에디터에 완전 몰입하기!

 

그리고 전체 평가도 해야하고 주간보고서도 작성해야하기 때문에

&&&& 스토리보드 만들어야 하고 각 페이지마다...ㅎㅎㅎ;;;;

거기에 PPT 도 수정된 핵심기능들을 정리해서 다시 만들어야한다 ㅎㅎ

내 손목이여~! 

수고를 부탁합니다!!!

 

그래도 오랜만에 코드랑 노니까 재밌다~ ㅋㅋㅋㅋ

몇 일동안 코드만지니까 말을 적게 해서 목도 좀 쉬는 기분 ㅎㅎ 

가끔은 이럴 때도 필요한 것 같다 ^__________^ 브이~파이팅!