본문 바로가기

카테고리 없음

Fiesta Tour_11일차

채팅 양 방향 실시간 기능 및 리스트

현재 투어 이미지 + 투어 제목으로 보이지만 메시지 보낸 사람 프로필 이미지 + 닉네임으로 수정 예정

-> 어떤 투어에관한 채팅인지는 채팅페이지에서 확인 가능 , 채팅 리스트도 별도 페이지 만들어서 분리 예정

'use client';

import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { API_MYPAGE_CHATS, API_POST_DETAILS } from '@/utils/apiConstants';
import axios from 'axios';

type ChatListProps = {
  userId: string;
};

type Chat = {
  post_id: string;
  sender_id: string;
  receiver_id: string;
  content: string;
  created_at: string;
};

type Post = {
  id: string;
  title: string;
  image: string;
};

const ChatList = ({ userId }: ChatListProps) => {
  const router = useRouter();

  const fetchPostDetails = async (postId: string): Promise<Post> => {
    const response = await axios.get(API_POST_DETAILS(postId));

    return response.data;
  };

  const {
    data: chatData,
    error: chatError,
    isLoading: chatLoading
  } = useQuery<Chat[]>({
    queryKey: ['chatList', userId],
    queryFn: async () => {
      const response = await axios.get(API_MYPAGE_CHATS(userId));

      return response.data;
    }
  });

  const postIds = chatData?.map((chat) => chat.post_id) || [];

  const {
    data: postData,
    error: postError,
    isLoading: postLoading
  } = useQuery<Post[]>({
    queryKey: ['postDetails', postIds],
    queryFn: async () => {
      const postDetails = await Promise.all(postIds.map((postId) => fetchPostDetails(postId)));
      return postDetails;
    },
    enabled: postIds.length > 0
  });

  if (chatLoading || postLoading) return <div>Loading...</div>;
  if (chatError || postError) return <div>Error loading data</div>;

  return (
    <div>
      {chatData?.map((chat: Chat, index: number) => {
        const postDetails = postData?.find((post) => post.id === chat.post_id);
        const receiverId = userId === chat.receiver_id ? chat.sender_id : chat.receiver_id;

        return (
          <div
            key={index}
            onClick={() =>
              router.push(
                `/${userId}/${receiverId}/chatpage?postId=${chat.post_id}&postTitle=${postDetails?.title}&postImage=${postDetails?.image}`
              )
            }
          >
            {postDetails && (
              <div className="flex">
                <Image
                  className="mr-4"
                  src={postDetails.image ?? '/icons/upload.png'}
                  alt={postDetails.title ?? 'Default title'}
                  width={40}
                  height={40}
                />
                <p>{postDetails.title}</p>
              </div>
            )}
            <p>{chat.content}</p>
            <p>{new Date(chat.created_at).toLocaleString()}</p>
          </div>
        );
      })}
    </div>
  );
};

export default ChatList;

현재 params를 통해 보내는 사람 , 받는 사람 , 게시글 id를 능동적으로 바꾸며 모두 같은 테이블에 저장 되지만 원하는 메시지만 때에 맞게 주고 받을 수 있으며 , 1초 간격으로 메시지를 새로 불러와 마치 실시간 채팅처럼 느껴지게 구현

-> 동시에 여러 사람이 이용 시 서버 부하가 어느정도일지는 확인 필요

import { useEffect, useState } from 'react';
import { fetchMessages, sendMessage } from '../services/chatService';

type Message = {
  id: string;
  sender_id: string;
  receiver_id: string;
  content: string;
  created_at: string;
  post_id: string;
};

type ChatProps = {
  senderId: string;
  receiverId: string;
  postId: string;
};

const Chat: React.FC<ChatProps> = ({ senderId, receiverId, postId }) => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [newMessage, setNewMessage] = useState<string>('');

  useEffect(() => {
    const interval = setInterval(async () => {
      const fetchedMessages = await fetchMessages(senderId, receiverId);
      setMessages(fetchedMessages);
    }, 1000);

    return () => clearInterval(interval);
  }, [senderId, receiverId]);

  const handleSend = async () => {
    if (newMessage.trim()) {
      await sendMessage(senderId, receiverId, newMessage, postId);
      setNewMessage('');
      const fetchedMessages = await fetchMessages(senderId, receiverId);
      setMessages(fetchedMessages);
    }
  };

  return (
    <div className="flex h-screen max-w-[360px] flex-col border border-gray-300 text-[14px]">
      <div className="overflow-y-auto p-4">
        {messages.map((msg) => (
          <div
            key={msg.id}
            className={`mb-10 flex flex-col ${msg.sender_id === senderId ? 'items-end' : 'items-start'}`}
          >
            {msg.sender_id !== senderId && <p className="mr-2 text-sm font-bold">{msg.sender_id}</p>}
            <div
              className={`max-w-[240px] break-all rounded p-2 ${msg.sender_id === senderId ? 'bg-green-200' : 'bg-gray-200'}`}
            >
              <p>{msg.content}</p>
            </div>
            <p className={` ${msg.sender_id === senderId ? 'right-0' : 'left-0'} mt-2 text-[10px] text-gray-500`}>
              {new Date(msg.created_at).toLocaleString()}
            </p>
          </div>
        ))}
      </div>
      <div className="flex border-t border-gray-300 p-4">
        <input
          type="text"
          value={newMessage}
          onChange={(e) => setNewMessage(e.target.value)}
          className="flex-1 rounded border border-gray-300 p-2"
        />
        <button onClick={handleSend} className="ml-2 rounded bg-blue-500 p-2 text-white">
          Send
        </button>
      </div>
    </div>
  );
};

export default Chat;

 

기존 코드에서는 유저 양 방향으로 메시지가 이동하는 것을 제대로 설계하지 못해 params를 이용하는걸 리셋 후 db만 통하게끔 구현하려 했으나 다행히도 조건문을 통하여 제대로 동작이 수행되고

url에 id를 포함한 정보들이 노출되므로 다른 유저가 접근하지 못하도록 인가 절차도 추가

 

초반 설계 미스로 관련 테이블에 변화도 생기고 로직도 조금씩 변하였는데 , 채팅 정보를 받아오는 api가 클라이언트 , 서버 별로 따로 있고 리스트를 불러오는 api도 따로 있다보니 채팅에 접근 가능한 상세페이지 , 마이페이지 ( 임시 ) 등 수정이 필요한 파일이 다수 있었음

-> 추후 api , 로직 등 유지보수의 효율 향상을 위해 리팩토링을 통해 코드 개선이 필요 할 것으로 보여 회의 예정