);
}
diff --git a/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx b/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx
index b29655cc..ebb1d0b5 100644
--- a/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx
+++ b/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx
@@ -18,7 +18,7 @@ function ChatMessage({
time,
onProfileClick,
}: ChatMessageProps) {
- const { senderId, content, sender, profileImage } = message;
+ const { userId, content, userNickname } = message;
return isMyMessage ? (
@@ -28,10 +28,9 @@ function ChatMessage({
props={{
content,
time,
- name: sender,
- profileImage: profileImage || '',
- isHost: senderId === hostId,
- onProfileClick: () => onProfileClick?.(senderId),
+ name: userNickname,
+ isHost: userId === hostId,
+ onProfileClick: () => onProfileClick?.(userId),
isConsecutive,
}}
/>
diff --git a/src/features/chat-room/container/chat-bubble-list/components/SystemMessage.tsx b/src/features/chat-room/container/chat-bubble-list/components/SystemMessage.tsx
index 1126ba25..9ab4d5bd 100644
--- a/src/features/chat-room/container/chat-bubble-list/components/SystemMessage.tsx
+++ b/src/features/chat-room/container/chat-bubble-list/components/SystemMessage.tsx
@@ -13,7 +13,7 @@ function SystemMessage({ message }: SystemMessageProps) {
variant="SYSTEM"
props={{
username: user,
- action: type === 'join' ? 'JOIN' : 'LEAVE',
+ action: type === 'JOIN' ? 'JOIN' : 'LEAVE',
}}
/>
);
diff --git a/src/features/chat-room/hooks/useMessageRenderer.ts b/src/features/chat-room/hooks/useMessageRenderer.ts
index 7b233ca2..43978598 100644
--- a/src/features/chat-room/hooks/useMessageRenderer.ts
+++ b/src/features/chat-room/hooks/useMessageRenderer.ts
@@ -21,15 +21,14 @@ export const useMessageRenderer = ({
currentMessage: ChatMessageType,
prevMessage?: Message,
) => {
- if (!prevMessage || prevMessage.type !== 'chat') return false;
+ if (!prevMessage || prevMessage.type !== 'CHAT') return false;
const timeDiff =
new Date(currentMessage.date).getTime() -
new Date(prevMessage.date).getTime();
return (
- prevMessage.senderId === currentMessage.senderId &&
- timeDiff < 10 * 60 * 1000
+ prevMessage.userId === currentMessage.userId && timeDiff < 10 * 60 * 1000
);
};
@@ -39,7 +38,7 @@ export const useMessageRenderer = ({
index: number,
) => ({
message,
- isMyMessage: user?.id == message.senderId,
+ isMyMessage: user?.id == message.userId,
isConsecutive: isConsecutiveMessage(message, messages[index - 1]),
hostId,
time: format(new Date(message.date), 'HH:mm'),
diff --git a/src/features/chat-room/types/chatBubbleList.ts b/src/features/chat-room/types/chatBubbleList.ts
index e53aa3a8..e5178116 100644
--- a/src/features/chat-room/types/chatBubbleList.ts
+++ b/src/features/chat-room/types/chatBubbleList.ts
@@ -1,18 +1,18 @@
export interface BaseMessage {
- type: 'chat' | 'join' | 'leave';
+ type: 'CHAT' | 'JOIN' | 'LEAVE';
date: string;
+ id: number;
}
export interface ChatMessageType extends BaseMessage {
- type: 'chat';
- sender: string;
- senderId: string | number;
+ type: 'CHAT';
+ userNickname: string;
+ userId: number;
content: string;
- profileImage?: string;
}
export interface SystemMessageType extends BaseMessage {
- type: 'join' | 'leave';
+ type: 'JOIN' | 'LEAVE';
user: string;
}
diff --git a/src/features/chat/components/ChatComponents.tsx b/src/features/chat/components/ChatComponents.tsx
new file mode 100644
index 00000000..c328ba53
--- /dev/null
+++ b/src/features/chat/components/ChatComponents.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import { useState } from 'react';
+import { sendMessage } from '../utils/socket';
+
+const ChatComponent = () => {
+ const [message, setMessage] = useState('');
+ const roomId = 44;
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (message.trim()) {
+ sendMessage(roomId, message);
+ setMessage('');
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default ChatComponent;
diff --git a/src/features/chat/components/chat-card/ChatCard.tsx b/src/features/chat/components/chat-card/ChatCard.tsx
index 36666cc7..4d20f415 100644
--- a/src/features/chat/components/chat-card/ChatCard.tsx
+++ b/src/features/chat/components/chat-card/ChatCard.tsx
@@ -32,7 +32,7 @@ function ChatCardBox({
return (
-
- 채팅에 활발히
-
- 참여해 보세요!
-
-
- );
-}
-
-export default ChatHeader;
diff --git a/src/features/chat/container/BookClubChatContainer.tsx b/src/features/chat/container/BookClubChatContainer.tsx
index f1f17226..66e7cb2b 100644
--- a/src/features/chat/container/BookClubChatContainer.tsx
+++ b/src/features/chat/container/BookClubChatContainer.tsx
@@ -1,125 +1,77 @@
'use client';
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import ChatCard from '@/features/chat/components/chat-card/ChatCard';
-
-interface BookClubChatData {
- imageUrl: string;
- isHost: boolean;
- title: string;
- currentParticipants: number;
- lastMessage: string;
- lastMessageTime: string;
- unreadCount: number;
-}
+import { bookClubs } from '@/api/book-club/react-query';
+import { useQuery } from '@tanstack/react-query';
+import { BookClubProps } from '@/features/chat/components/chat-card/types/variantChatCard';
+import Link from 'next/link';
+import { getRecentChats, getStompClient } from '@/features/chat/utils/socket';
+import { findRecentMessage } from '@/features/chat/utils/chatRoom';
+import { formatDateForUI } from '@/lib/utils/formatDateForUI';
export default function BookClubChatContainer() {
- const mockBookClubChats: BookClubChatData[] = [
- {
- imageUrl: '/images/profile.png',
- isHost: true,
- title: '철학으로 보는 현대사회',
- currentParticipants: 8,
- lastMessage: '다음 모임은 니체의 차라투스트라를 읽어볼까요?',
- lastMessageTime: '11:30',
- unreadCount: 5,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: false,
- title: 'SF 소설 읽기 모임',
- currentParticipants: 12,
- lastMessage: '듄 시리즈 완독하신 분들 계신가요?',
- lastMessageTime: '09:15',
- unreadCount: 2,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: true,
- title: '심리학 북클럽',
- currentParticipants: 6,
- lastMessage: '이번주 토론 주제는 프로이트의 꿈의 해석입니다.',
- lastMessageTime: '어제',
- unreadCount: 0,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: false,
- title: '시 읽는 청춘들',
- currentParticipants: 15,
- lastMessage: '이상의 시를 함께 감상해보아요',
- lastMessageTime: '어제',
- unreadCount: 7,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: false,
- title: '경제경영 독서모임',
- currentParticipants: 10,
- lastMessage: '이번 주 도서: 넛지',
- lastMessageTime: '2일 전',
- unreadCount: 1,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: true,
- title: '세계문학 산책',
- currentParticipants: 7,
- lastMessage: '카프카 변신 읽고 오세요!',
- lastMessageTime: '3일 전',
- unreadCount: 4,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: false,
- title: '에세이 교환일기',
- currentParticipants: 4,
- lastMessage: '무라카미 하루키의 에세이집 추천합니다',
- lastMessageTime: '1주일 전',
- unreadCount: 0,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: true,
- title: '미스터리 소설 탐독',
- currentParticipants: 9,
- lastMessage: '애거사 크리스티 특집 시작합니다',
- lastMessageTime: '1주일 전',
- unreadCount: 12,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: false,
- title: '인문학 스터디',
- currentParticipants: 11,
- lastMessage: '유발 하라리의 사피엔스 함께 읽어요',
- lastMessageTime: '2주일 전',
- unreadCount: 3,
- },
- {
- imageUrl: '/images/profile.png',
- isHost: true,
- title: '고전문학 독파',
- currentParticipants: 6,
- lastMessage: '돈키호테 완독 인증하신 분들 계신가요?',
- lastMessageTime: '2주일 전',
- unreadCount: 8,
- },
- ];
+ const [recentMessages, setRecentMessages] = useState<
+ Array<{
+ bookClubId: number;
+ content: string;
+ date: string;
+ }>
+ >([]);
+
+ const { data, isLoading, error } = useQuery(
+ bookClubs.my()._ctx.joined({ order: 'DESC', page: 1, size: 10 }),
+ );
+
+ useEffect(() => {
+ const fetchRecentChats = async () => {
+ try {
+ const response = await getRecentChats();
+ console.log('최근 채팅 내용 조회 성공:', response);
+ setRecentMessages(response || []);
+ } catch (error) {
+ console.error('최근 채팅 내용 조회 실패:', error);
+ }
+ };
+
+ try {
+ getStompClient();
+ fetchRecentChats();
+ } catch (error) {
+ console.error('소켓이 초기화되지 않았습니다:', error);
+ }
+ }, []);
+
+ const bookClubChats = data?.bookClubs || [];
+ console.log('bookClubChats', bookClubChats);
+
+ if (isLoading) return
로딩중...
;
+ if (error) return
에러가 발생했습니다
;
return (
- {mockBookClubChats.map((chat, index) => (
- console.log('채팅방 클릭'),
- }}
- />
- ))}
+ {bookClubChats.map((bookClub: BookClubProps, id: number) => {
+ const recentMessage = findRecentMessage(
+ recentMessages,
+ Number(bookClub.id),
+ );
+
+ return (
+
+
+
+ );
+ })}
);
diff --git a/src/features/chat/container/ChatContainer.tsx b/src/features/chat/container/ChatContainer.tsx
index 634ef52a..8400ba97 100644
--- a/src/features/chat/container/ChatContainer.tsx
+++ b/src/features/chat/container/ChatContainer.tsx
@@ -1,14 +1,31 @@
'use client';
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import Tab from '@/components/tab/Tab';
import { CONTENT_TABS, contentTab } from '@/constants';
import BookClubChatContainer from './BookClubChatContainer';
import ExchangeChatContainer from './ExchangeChatContainer';
+import { initializeSocket } from '../utils/socket';
+import { getCookie } from '@/features/auth/utils/cookies';
export default function ChatContainer() {
const [selectedTab, setSelectedTab] = useState
(CONTENT_TABS.CLUB);
+ useEffect(() => {
+ const token = getCookie('auth_token');
+ if (token) {
+ const connectSocket = async () => {
+ try {
+ await initializeSocket(token);
+ console.log('채팅 페이지에서 소켓 재연결 성공');
+ } catch (error) {
+ console.error('채팅 페이지에서 소켓 재연결 실패:', error);
+ }
+ };
+ connectSocket();
+ }
+ }, []);
+
return (
<>
diff --git a/src/features/chat/utils/chatRoom.ts b/src/features/chat/utils/chatRoom.ts
new file mode 100644
index 00000000..22334fb1
--- /dev/null
+++ b/src/features/chat/utils/chatRoom.ts
@@ -0,0 +1,10 @@
+export const findRecentMessage = (
+ recentMessages: Array<{
+ bookClubId: number;
+ content: string;
+ date: string;
+ }>,
+ chatId: number,
+) => {
+ return recentMessages.find((msg) => msg.bookClubId === chatId);
+};
diff --git a/src/features/chat/utils/socket.ts b/src/features/chat/utils/socket.ts
new file mode 100644
index 00000000..de1d5cb4
--- /dev/null
+++ b/src/features/chat/utils/socket.ts
@@ -0,0 +1,197 @@
+import SockJS from 'sockjs-client';
+import { Client } from '@stomp/stompjs';
+import { BookClub } from '@/types/bookclubs';
+import apiClient from '@/lib/utils/apiClient';
+
+let stompClient: Client | null = null;
+
+export interface ChatMessage {
+ id: number;
+ bookClubId: number;
+ date: string;
+ userId: number;
+ userNickname: string;
+ type: 'CHAT' | 'JOIN' | 'LEAVE';
+ content: string;
+ user?: string;
+}
+
+export interface HistoryResponse {
+ date: string;
+ messages: ChatMessage[];
+}
+
+export interface ChatHistoryResponse {
+ historyResponses: HistoryResponse[];
+}
+
+export const initializeSocket = async (token: string) => {
+ if (stompClient?.connected) {
+ console.log('이미 연결된 소켓이 있습니다.');
+ return stompClient;
+ }
+
+ try {
+ const response = await apiClient.get('/book-clubs/my-joined', {
+ params: {
+ order: 'DESC',
+ size: 10,
+ page: 1,
+ },
+ });
+
+ const { bookClubs } = response.data;
+
+ const client = new Client({
+ webSocketFactory: () =>
+ new SockJS(`${process.env.NEXT_PUBLIC_API_URL}/ws?token=${token}`),
+ reconnectDelay: 5000,
+ heartbeatIncoming: 4000,
+ heartbeatOutgoing: 4000,
+ });
+
+ client.onConnect = () => {
+ console.log('소켓 연결 성공');
+
+ bookClubs.forEach((club: BookClub) => {
+ console.log('구독 시도:', club.id);
+ client.subscribe(`/topic/group-chat/${club.id}`, (message) => {
+ const chatMessage: ChatMessage = JSON.parse(message.body);
+ console.log(`모임 ${club.title}의 새 메시지 수신:`, chatMessage);
+ });
+ });
+ };
+
+ client.onDisconnect = () => {
+ console.log('소켓 연결 끊김');
+ };
+
+ client.onStompError = (frame) => {
+ console.error('Stomp 에러:', frame);
+ };
+
+ client.activate();
+ stompClient = client;
+
+ return client;
+ } catch (error) {
+ console.error('모임 정보 조회 실패:', error);
+ throw error;
+ }
+};
+
+export const disconnectSocket = () => {
+ if (stompClient) {
+ stompClient.deactivate();
+ stompClient = null;
+ }
+};
+
+export const getStompClient = () => {
+ if (!stompClient) {
+ throw new Error('소켓이 초기화되지 않았습니다.');
+ }
+ return stompClient;
+};
+
+export const sendMessage = (roomId: number, content: string) => {
+ const client = getStompClient();
+
+ if (content === '') {
+ return;
+ }
+
+ console.log(`메시지 전송 시도: roomId=${roomId}, content=${content}`);
+
+ client.publish({
+ destination: `/app/group-chat/${roomId}/sendMessage`,
+ body: JSON.stringify({ content }),
+ });
+ console.log('메시지 전송 성공');
+};
+
+export const getChatHistory = (roomId: number) => {
+ const client = getStompClient();
+
+ return new Promise((resolve, reject) => {
+ const subscription = client.subscribe(
+ '/user/queue/chatHistory',
+ (message) => {
+ try {
+ const historyData: ChatHistoryResponse = JSON.parse(message.body);
+ console.log('채팅 히스토리 수신:', historyData);
+ resolve(historyData);
+ subscription.unsubscribe();
+ } catch (error) {
+ console.error('채팅 히스토리 파싱 실패:', error);
+ reject(error);
+ subscription.unsubscribe();
+ }
+ },
+ );
+
+ try {
+ client.publish({
+ destination: `/app/group-chat/history/${roomId}`,
+ body: JSON.stringify({}),
+ });
+ console.log('채팅 히스토리 요청 전송');
+ } catch (error) {
+ console.error('채팅 히스토리 요청 실패:', error);
+ subscription.unsubscribe();
+ reject(error);
+ }
+ });
+};
+
+export const getRecentChats = () => {
+ const client = getStompClient();
+ console.log('getRecentChats - 클라이언트 상태:', {
+ connected: client.connected,
+ active: client.active,
+ });
+
+ return new Promise((resolve, reject) => {
+ const subscription = client.subscribe('/user/queue/recent', (message) => {
+ console.log('구독 콜백 실행됨');
+ try {
+ const recentData: ChatMessage[] = JSON.parse(message.body);
+ console.log('최신 채팅 데이터:', recentData);
+ resolve(recentData);
+ subscription.unsubscribe();
+ } catch (error) {
+ console.error('최신 채팅 파싱 실패:', error);
+ reject(error);
+ subscription.unsubscribe();
+ }
+ });
+
+ try {
+ console.log('서버로 요청 전송 시도');
+ client.publish({
+ destination: '/app/group-chat/recent',
+ body: JSON.stringify({}),
+ });
+ console.log('서버로 요청 전송 완료');
+ } catch (error) {
+ console.error('최신 채팅 요청 실패:', error);
+ subscription.unsubscribe();
+ reject(error);
+ }
+ });
+};
+
+export const subscribeToChat = (
+ roomId: number,
+ callback: (message: ChatMessage) => void,
+) => {
+ const client = getStompClient();
+ return client.subscribe(`/topic/group-chat/${roomId}`, (message) => {
+ const chatMessage: ChatMessage = JSON.parse(message.body);
+ callback(chatMessage);
+ });
+};
+
+export const isSocketConnected = () => {
+ return stompClient?.connected ?? false;
+};
diff --git a/src/lib/utils/formatDateForUI.ts b/src/lib/utils/formatDateForUI.ts
index 351e94fd..91f3f19b 100644
--- a/src/lib/utils/formatDateForUI.ts
+++ b/src/lib/utils/formatDateForUI.ts
@@ -1,6 +1,6 @@
export const formatDateForUI = (
isoString: string,
- outputFormat: 'KOREAN' | 'DATE_ONLY',
+ outputFormat: 'KOREAN' | 'DATE_ONLY' | 'CHAT_ROOM',
): string => {
const date = new Date(isoString);
@@ -21,6 +21,8 @@ export const formatDateForUI = (
return `${month}/${day}(${dayOfWeek}) ${meridiem} ${hour}:${minute}`;
case 'DATE_ONLY':
return `${year}.${month}.${day}`;
+ case 'CHAT_ROOM':
+ return `${month}.${day} ${meridiem} ${hour}:${minute}`;
}
};
diff --git a/src/middleware.ts b/src/middleware.ts
index b56b78fc..f21e0794 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -38,4 +38,5 @@ export async function middleware(request: NextRequest) {
export const config = {
matcher: ['/wish', '/profile', '/login', '/bookclub/create', '/chat'],
+ unstable_allowDynamic: ['**/node_modules/sockjs-client/**'],
};