juni
WelKo_채팅 확인 여부 구현 본문
확인 한 채팅 알림 사라지는 기능 구현 중
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;
};
'프로젝트 > WelKo' 카테고리의 다른 글
WelKo_예약 내역 UI 개선 (0) | 2024.08.10 |
---|---|
WelKo_마이 페이지 UI 및 Server 코드 개선 (0) | 2024.08.09 |
WelKo_중간 발표 및 피드백 (0) | 2024.08.05 |
WelKo_닉네임 수정 (0) | 2024.08.04 |
WelKo_찜 카테고리 (0) | 2024.08.02 |