Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/message/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const INITIAL_PAGE_SIZE = 10;
export default async function MessagePage() {
const cookieStore = await cookies();
const userId = Number(cookieStore.get('userId')?.value || 0);

const accessToken = cookieStore.get('accessToken')?.value || null;
const queryClient = new QueryClient();

// 첫 페이지 우선 prefetch
Expand All @@ -35,7 +35,7 @@ export default async function MessagePage() {

return (
<HydrationBoundary state={dehydratedState}>
<FollowingContent initialUserId={userId} />
<FollowingContent accessToken={accessToken} initialUserId={userId} />
</HydrationBoundary>
);
}
25 changes: 15 additions & 10 deletions src/components/pages/chat/chat-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,40 @@
import Image from 'next/image';
import { useRouter } from 'next/navigation';

import { useEffect } from 'react';
import { useMemo } from 'react';

import { useQueryClient } from '@tanstack/react-query';
import { DEFAULT_PROFILE_IMAGE } from 'constants/default-images';

import { useGetChatList } from '@/hooks/use-chat/use-chat-list';
import { useChatListSocket, useGetChatList } from '@/hooks/use-chat';
import { cn } from '@/lib/utils';

import { ChattingNone } from '../chat-none';

interface IProps {
userId: number;
accessToken: string | null;
}

export const ChatList = ({ userId }: IProps) => {
export const ChatList = ({ userId, accessToken }: IProps) => {
const router = useRouter();
const queryClient = useQueryClient();
const handleClick = (chatId: number) => {
router.push(`/chat/${chatId}`);
};
const { data: chatList } = useGetChatList({ userId });

console.log(chatList);

// 현재 방식은 tanstack query를 이용해서 단지 목록 조회
// but, 소켓 통신을 하고 있는 상황이므로 목록 역시 소켓을 열어서 페이지에 머물러 있을 때도 실시간으로 확인 가능하도록
useEffect(() => {
queryClient.invalidateQueries({ queryKey: ['chatList', userId] });
}, [chatList, userId]);
// 채팅방 ID 목록 추출
const chatRoomIds = useMemo(() => {
return chatList?.chatRooms?.map((chat) => chat.chatRoomId) || [];
}, [chatList]);

// 모든 채팅방 구독하여 실시간 갱신
useChatListSocket({
userId,
accessToken,
chatRoomIds,
});

return (
<ul className='flex flex-col'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ const SOCIAL_TABS = [

interface FollowingContentProps {
initialUserId: number;
accessToken: string | null;
}

export const FollowingContent = ({ initialUserId }: FollowingContentProps) => {
export const FollowingContent = ({ initialUserId, accessToken }: FollowingContentProps) => {
const params = useSearchParams();
const tab = params.get('tab') || 'chat';

Expand Down Expand Up @@ -59,7 +60,7 @@ export const FollowingContent = ({ initialUserId }: FollowingContentProps) => {
<div className='min-h-screen bg-[#F1F5F9]'>
<TabNavigation basePath='/message' defaultValue='chat' tabs={SOCIAL_TABS} />

{tab === 'chat' && <ChatList userId={initialUserId} />}
{tab === 'chat' && <ChatList accessToken={accessToken} userId={initialUserId} />}
{tab === 'following' && (
<>
<FollowingSearch userId={initialUserId} />
Expand Down
1 change: 1 addition & 0 deletions src/hooks/use-chat/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { useCreateDMChat } from './use-chat-dm';
export { useGetChatList } from './use-chat-list';
export { useChatListSocket } from './use-chat-list-socket';
export { useLongText } from './use-chat-longText';
export { useGetChatMessages } from './use-chat-messages';
export { useChatSocket } from './use-chat-socket';
72 changes: 72 additions & 0 deletions src/hooks/use-chat/use-chat-list-socket/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useEffect, useRef } from 'react';

import { Client, IMessage, StompSubscription } from '@stomp/stompjs';
import { useQueryClient } from '@tanstack/react-query';
import SockJS from 'sockjs-client';

interface UseChatListSocketOptions {
userId: number;
accessToken: string | null;
chatRoomIds: number[]; // 구독할 채팅방 ID 목록
}

export const useChatListSocket = ({
userId,
accessToken,
chatRoomIds,
}: UseChatListSocketOptions) => {
const clientRef = useRef<Client | null>(null);
const queryClient = useQueryClient();
const subscriptionsRef = useRef<Map<number, StompSubscription>>(new Map());

useEffect(() => {
if (!accessToken || chatRoomIds.length === 0) return;

const client = new Client({
webSocketFactory: () => {
const socket = new SockJS(`${process.env.NEXT_PUBLIC_API_BASE_URL}/ws-chat`, null, {
transports: ['websocket'],
});
return socket;
},
connectHeaders: {
Authorization: `Bearer ${accessToken}`,
},
reconnectDelay: 6000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});

client.onConnect = () => {
console.log('✅ Chat list socket connected');

// 모든 채팅방 구독
chatRoomIds.forEach((roomId) => {
const subscription = client.subscribe(`/sub/chat/room/${roomId}`, (message: IMessage) => {
const payload = JSON.parse(message.body);
console.log('🔔 새 메시지 수신:', payload);

// 채팅 목록 갱신
queryClient.invalidateQueries({
queryKey: ['chatList', userId],
exact: true,
});
});

subscriptionsRef.current.set(roomId, subscription);
});
};

client.activate();
clientRef.current = client;

return () => {
// 모든 구독 해제
subscriptionsRef.current.forEach((subscription) => {
subscription.unsubscribe();
});
subscriptionsRef.current.clear();
client.deactivate();
};
}, [userId, accessToken, chatRoomIds, queryClient]);
};