카테고리 없음

Fiesta Tour_17일차

juni_shin 2024. 8. 7. 23:17

확인 한 채팅 알림 사라지는 기능 구현 중

import React, { useEffect, useState } 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, API_MYPAGE_PROFILE } from '@/utils/apiConstants';
import axios from 'axios';
import { fetchMessages } from '@/services/chatService';

type ChatListProps = {
  userId: string;
};

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

type Chat = {
  post_id: string;
  sender_id: string;
  receiver_id: string;
  messages: Message[];
};

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

type User = {
  id: string;
  name: string;
  avatar: string;
};

const ChatList = ({ userId }: ChatListProps) => {
  const [newMessages, setNewMessages] = useState<{ [key: string]: boolean }>({});
  const router = useRouter();

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

  const fetchUserDetails = async (userId: string): Promise<User> => {
    const response = await axios.get(API_MYPAGE_PROFILE(userId));
    return response.data;
  };

  const {
    data: chatData,
    error: chatError,
    isPending: chatPending
  } = useQuery<Message[]>({
    queryKey: ['chatList', userId],
    queryFn: async () => {
      const response = await axios.get(API_MYPAGE_CHATS(userId));
      return response.data;
    },
    refetchInterval: 5000
  });

  const postIds = chatData?.map((chat) => chat.post_id) || [];
  const userIds = chatData ? chatData.flatMap((chat) => [chat.sender_id, chat.receiver_id]) : [];

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

  const {
    data: userData,
    error: userError,
    isPending: userPending
  } = useQuery<User[]>({
    queryKey: ['userDetails', userIds],
    queryFn: async () => {
      const userDetails = await Promise.all(userIds.map((id) => fetchUserDetails(id)));
      return userDetails;
    },
    enabled: userIds.length > 0
  });

  const groupedChats = chatData?.reduce((acc: { [key: string]: Chat }, message) => {
    const chatId = `${message.post_id}-${[message.sender_id, message.receiver_id].sort().join('-')}`;
    if (!acc[chatId]) {
      acc[chatId] = {
        post_id: message.post_id,
        sender_id: message.sender_id,
        receiver_id: message.receiver_id,
        messages: []
      };
    }
    acc[chatId].messages.push(message);
    return acc;
  }, {});

  const handleChatClick = async (chat: Chat) => {
    const receiverId = userId === chat.sender_id ? chat.receiver_id : chat.sender_id;
    const postDetails = postData?.find((post) => post.id === chat.post_id);
    const chatId = `${chat.post_id}-${[chat.sender_id, chat.receiver_id].sort().join('-')}`;

    setNewMessages((prev) => ({
      ...prev,
      [chatId]: true
    }));

    await fetchMessages(chat.receiver_id, chat.sender_id, chat.post_id);

    router.push(
      `/${userId}/${receiverId}/chatpage?postId=${chat.post_id}&postTitle=${postDetails?.title}&postImage=${postDetails?.image}`
    );
  };

  if (chatPending || postPending || userPending) return <div>Loading...</div>;
  if (chatError || postError || userError) return <div>Error loading data</div>;

  return (
    <div>
      {groupedChats &&
        Object.values(groupedChats).map((chat, index) => {
          const postDetails = postData?.find((post) => post.id === chat.post_id);
          const receiverId = userId === chat.sender_id ? chat.receiver_id : chat.sender_id;
          const senderDetails = userData?.find((user) => user.id === receiverId);

          const firstMessage = chat.messages[0];
          const chatId = `${chat.post_id}-${[chat.sender_id, chat.receiver_id].sort().join('-')}`;
          const isNewMessage = !newMessages[chatId] && firstMessage.sender_id !== userId && !firstMessage.is_checked;

          return (
            <div className="mb-[32px] max-w-[360px]" key={index} onClick={() => handleChatClick(chat)}>
              {postDetails && senderDetails && (
                <div className="flex">
                  <Image
                    className="rounded"
                    src={postDetails.image || '/icons/upload.png'}
                    alt={postDetails.title || 'Default name'}
                    width={64}
                    height={64}
                    style={{ width: '64px', height: '64px' }}
                  />
                  <div className="ml-[8px] flex w-full flex-col gap-[5px]">
                    <div className="flex items-center justify-between">
                      <div className="mx-auto max-w-[360px]">
                        <p className="line-clamp-1 text-[13px] font-medium">{postDetails.title}</p>
                      </div>
                      <p className="text-[10px] text-grayscale-500">
                        {new Date(firstMessage?.created_at).toLocaleString()}
                      </p>
                    </div>
                    <div className="flex items-center justify-between">
                      <p className="text-[12px]">{firstMessage?.content}</p>
                      {isNewMessage && <span className="h-[8px] w-[8px] rounded-full bg-action-color"></span>}
                    </div>
                    <div className="flex">
                      <Image
                        className="items-center rounded-full"
                        src={senderDetails.avatar || '/icons/upload.png'}
                        alt={senderDetails.name || 'Default name'}
                        width={16}
                        height={16}
                        style={{ width: '16px', height: '16px' }}
                      />
                      <p className="ml-[4px] text-[10px] text-grayscale-500">{senderDetails.name}</p>
                    </div>
                  </div>
                </div>
              )}
            </div>
          );
        })}
    </div>
  );
};

export default ChatList;

 

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

type User = {
  id: string;
  name: string;
  avatar: string;
};

type Message = {
  id: string;
  sender_id: string;
  receiver_id: string;
  content: string;
  created_at: string;
  post_id: string;
  is_checked: boolean;
  sender: User;
  receiver: User;
};

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>('');

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

  useEffect(() => {
    const fetchData = async () => {
      const fetchedMessages = await fetchMessages(senderId, receiverId, postId);
      setMessages(fetchedMessages);
    };

    const interval = setInterval(fetchData, 1000);
    return () => clearInterval(interval);
  }, [senderId, receiverId, postId]);

  return (
    <div className="flex h-screen flex-col text-[14px]">
      <div className="overflow-y-auto">
        {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 && msg.sender && (
              <div className="flex items-center">
                <Image
                  className="mr-2 rounded-full"
                  src={msg.sender.avatar}
                  alt="avatar"
                  width={44}
                  height={44}
                  style={{ width: '44px', height: '44px' }}
                />
                <p className="text-[13px] font-semibold">{msg.sender.name}</p>
              </div>
            )}
            <div
              className={`mt-[7px] max-w-[240px] break-all px-[8px] py-[12px] ${
                msg.sender_id === senderId
                  ? 'rounded-br-0 rounded-bl-[16px] rounded-tl-[16px] rounded-tr-[16px] bg-primary-50'
                  : 'rounded-bl-0 rounded-br-[16px] rounded-tl-[16px] rounded-tr-[16px] bg-grayscale-50'
              }`}
            >
              <p className="text-[14px]">{msg.content}</p>
            </div>
            <p
              className={` ${msg.sender_id === senderId ? 'right-0' : 'left-0'} mt-[4px] text-[10px] text-grayscale-500`}
            >
              {new Date(msg.created_at).toLocaleString()}
            </p>
          </div>
        ))}
      </div>
      <div className="container mx-auto flex items-center justify-between p-8">
        <input
          className="flex-1 rounded-[12px] border bg-grayscale-50 p-2 text-[16px]"
          type="text"
          placeholder="Placeholder text"
          value={newMessage}
          onChange={(e) => setNewMessage(e.target.value)}
        />
        <button
          className="mx-auto ml-[8px] flex h-[48px] w-[48px] items-center justify-center rounded-[24px] bg-primary-300"
          onClick={handleSend}
        >
          <Image
            src="/icons/tabler-icon-send.svg"
            alt="Send"
            width={24}
            height={24}
            style={{ width: '24px', height: '24px' }}
          />
        </button>
      </div>
    </div>
  );
};

export default Chat;

 

import { createClient } from '@/utils/supabase/client';

const supabase = createClient();

export const fetchMessages = async (senderId: string, receiverId: string, postId: string) => {
  const { data, error } = await supabase
    .from('messages')
    .select(
      `
      *,
      sender:users!messages_sender_id_fkey ( id, name, avatar ),
      receiver:users!messages_receiver_id_fkey ( id, name, avatar )
    `
    )
    .eq('post_id', postId)
    .or(
      `and(sender_id.eq.${senderId},receiver_id.eq.${receiverId}),and(sender_id.eq.${receiverId},receiver_id.eq.${senderId})`
    )
    .order('created_at', { ascending: true });

  if (error) {
    return [];
  }

  const uncheckedMessages = data.filter((message) => message.receiver_id === receiverId && !message.is_checked);
  if (uncheckedMessages.length > 0) {
    const uncheckedMessageIds = uncheckedMessages.map((message) => message.id);
    await supabase.from('messages').update({ is_checked: true }).in('id', uncheckedMessageIds);
  }

  return data;
};

export const sendMessage = async (senderId: string, receiverId: string, content: string, postId: string) => {
  const { data, error } = await supabase
    .from('messages')
    .insert([{ sender_id: senderId, receiver_id: receiverId, content, post_id: postId, is_checked: false }]);

  if (error) {
    return null;
  }

  return data;
};