diff --git a/src/components/common/ChatRoom.tsx b/src/components/common/ChatRoom.tsx index 66487c5..8a76d6c 100644 --- a/src/components/common/ChatRoom.tsx +++ b/src/components/common/ChatRoom.tsx @@ -1,61 +1,46 @@ import React, { useEffect, useState, useRef } from 'react'; import styled from 'styled-components'; import { FaRegComment } from 'react-icons/fa'; -import { - fetchChatRoomDetails, - fetchChatMessages, -} from '../pages/community/api/chatApi'; +import { fetchChatMessages } from '../pages/community/api/chatApi'; import { webSocketService } from '../../utils/webSocket'; interface Message { senderId: string; content: string; timestamp: string | null; - chatRoomId?: string; - type?: 'date'; -} - -interface Participant { - userId: string; - nickname: string; + type?: string; } interface ChatRoomProps { - chatRoomId: string; + chatRoomId: number; + chatMembers: string[]; webSocketService: typeof webSocketService; isAdmin?: boolean; // 관리자 여부 } const ChatRoom: React.FC = ({ chatRoomId, + chatMembers, webSocketService, isAdmin = false, }) => { const [messages, setMessages] = useState([]); - const [participants, setParticipants] = useState([]); const [input, setInput] = useState(''); - const currentUserId = 'user-00001'; // Mock 로그인 사용자 ID const chatBoxRef = useRef(null); - const [authorId, setAuthorId] = useState(''); + const currentUserId = 'user-00001'; // Mock 로그인 사용자 ID useEffect(() => { - // 채팅방 정보 및 초기 메시지 가져오기 - const fetchRoomDetails = async () => { + // 채팅방 초기 메시지 및 채팅 메시지 가져오기 + const fetchMessages = async () => { try { - const { participants, authorNickname } = - await fetchChatRoomDetails(chatRoomId); - setParticipants(participants); - - const author = participants.find((p) => p.nickname === authorNickname); - if (author) setAuthorId(author.userId); - - const participantNicknames = participants.map((p) => p.nickname); + const fetchedMessages = await fetchChatMessages(chatRoomId); // 입장 메시지 추가 const joinMessage: Message = { senderId: 'system', - content: `'${[...participantNicknames].join(', ')}'님께서 -채팅방에 입장하셨습니다.`, + content: `${chatMembers + .map((member) => getNicknameDisplay(member)) + .join(', ')}님이 입장하셨습니다.`, timestamp: null, // timestamp 표시하지 않음 }; @@ -91,45 +76,43 @@ const ChatRoom: React.FC = ({ timestamp: null, }; - const fetchedMessages = await fetchChatMessages(chatRoomId); setMessages([joinMessage, groupChatNotice, ...fetchedMessages]); } catch (error) { - console.error('Failed to fetch chat room details or messages:', error); + console.error('Failed to fetch messages:', error); } }; - fetchRoomDetails(); - }, [chatRoomId]); + fetchMessages(); + }, [chatRoomId, chatMembers]); const getNicknameDisplay = (senderId: string): string => { if (senderId === 'system') return ''; - const participant = participants.find((p) => p.userId === senderId); - if (!participant) return senderId; - - if (senderId === authorId) { - return senderId === currentUserId - ? '나(방장)' - : `${participant.nickname}(방장)`; - } - return senderId === currentUserId ? '나' : participant.nickname; + return senderId === currentUserId ? '나' : senderId; }; useEffect(() => { // WebSocket 연결 const handleIncomingMessage = (data: Message) => { - if (data.chatRoomId === chatRoomId) { - setMessages((prev) => [...prev, data]); - } + setMessages((prev) => [...prev, data]); }; webSocketService.connect( - handleIncomingMessage, - () => console.log('WebSocket connected'), + () => { + webSocketService.subscribe( + `/sub/message/${chatRoomId}`, + (messageOutput) => { + const data = JSON.parse(messageOutput.body); + handleIncomingMessage(data); + } + ); + console.log('WebSocket connected to room'); + }, () => console.log('WebSocket disconnected'), - (error) => console.error('WebSocket error:', error) + () => console.error('WebSocket connection error') ); return () => { + webSocketService.unsubscribe(`/sub/message/${chatRoomId}`); webSocketService.close(); }; }, [chatRoomId, webSocketService]); @@ -145,16 +128,15 @@ const ChatRoom: React.FC = ({ if (!input.trim()) return; const message: Message = { - senderId: isAdmin ? 'system' : currentUserId, // 관리자는 시스템 메시지로 보냄 + senderId: isAdmin ? 'system' : currentUserId, content: isAdmin ? `[관리자 메시지] ${input.trim()}` : input.trim(), - chatRoomId, timestamp: new Date().toISOString(), }; - // WebSocket으로 메시지 전송 - webSocketService.send(`/pub/message/${chatRoomId}`, message); - - // 메시지 상태 업데이트 + webSocketService.send( + `/pub/message/${chatRoomId}`, + JSON.stringify(message) + ); setMessages((prev) => [...prev, message]); setInput(''); }; diff --git a/src/components/pages/admin/ChatRoomManagementPage.tsx b/src/components/pages/admin/ChatRoomManagementPage.tsx index 05bb0f8..bb336e1 100644 --- a/src/components/pages/admin/ChatRoomManagementPage.tsx +++ b/src/components/pages/admin/ChatRoomManagementPage.tsx @@ -1,13 +1,9 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import ChatRoomModal from '../community/modal/ChatRoomModal'; // 기존 채팅 모달 컴포넌트 import Pagination from '../../common/Pagination'; // 기존 페이지네이션 컴포넌트 -import { - fetchChatRooms, - fetchChatRoomDetails, - deleteChatRoom, -} from '../community/api/chatApi'; +import { fetchChatRooms } from '../community/api/chatApi'; import { FaBackspace } from 'react-icons/fa'; import { webSocketService } from '../../../utils/webSocket'; @@ -15,21 +11,21 @@ const ITEMS_PER_PAGE = 12; // 페이지당 카드 수 const ChatRoomManagementPage = () => { const navigate = useNavigate(); + const location = useLocation(); const [chatRooms, setChatRooms] = useState< { - chatRoomId: string; - chatRoomTitle: string; - participants: { userId: string; nickname: string }[]; + postId: number; + roomName: string; + chatMembers: string[]; }[] >([]); // 채팅방 데이터 const [currentPage, setCurrentPage] = useState(1); - const [selectedRoom, setSelectedRoom] = useState<{ - chatRoomId: string; - chatRoomTitle: string; - participants: { userId: string; nickname: string }[]; - } | null>(null); // 선택된 채팅방 const [isModalOpen, setModalOpen] = useState(false); + // URL에서 chatRoomId 동적으로 가져오기 + const urlParams = new URLSearchParams(location.search); + const roomId = urlParams.get('roomId'); // URL에서 roomId 가져오기 + // 총 페이지 수 계산 const totalPages = Math.ceil(chatRooms.length / ITEMS_PER_PAGE); @@ -53,30 +49,13 @@ const ChatRoomManagementPage = () => { startIndex + ITEMS_PER_PAGE ); - const handleCardClick = async (chatRoomId: string) => { - try { - const roomDetails = await fetchChatRoomDetails(chatRoomId); // API 호출로 채팅방 세부 정보 가져오기 - setSelectedRoom(roomDetails); - setModalOpen(true); - } catch (error) { - console.error('채팅방 세부 정보를 가져오는 중 오류 발생:', error); - } + const handleCardClick = () => { + setModalOpen(true); // 모달 열기 }; - const handleDeleteRoom = async (chatRoomId: string) => { - if (window.confirm('채팅방을 정말 삭제하시겠습니까?')) { - try { - await deleteChatRoom(chatRoomId); // 채팅방 삭제 API 호출 - setChatRooms((prev) => - prev.filter((room) => room.chatRoomId !== chatRoomId) - ); - setModalOpen(false); - alert('채팅방이 삭제되었습니다.'); - } catch (error) { - console.error('채팅방 삭제 중 오류 발생:', error); - alert('채팅방 삭제에 실패했습니다.'); - } - } + const closeModal = () => { + setModalOpen(false); // 모달 닫기 + navigate(-1); // URL 복구 }; return ( @@ -94,13 +73,10 @@ const ChatRoomManagementPage = () => { {paginatedChatRooms.map((room) => ( - handleCardClick(room.chatRoomId)} - > - {room.chatRoomTitle} + + {room.roomName} - 참여자: {room.participants.map((p) => p.nickname).join(', ')} + 참여자: {room.chatMembers.join(', ')} ))} @@ -110,14 +86,16 @@ const ChatRoomManagementPage = () => { totalPages={totalPages} onPageChange={(page) => setCurrentPage(page)} /> - {isModalOpen && selectedRoom && ( + {isModalOpen && roomId && ( room.postId.toString() === roomId) + ?.roomName || '' + } isOpen={isModalOpen} - onClose={() => setModalOpen(false)} - webSocketService={webSocketService} // WebSocket 서비스 객체 필요 시 추가 - onDelete={() => handleDeleteRoom(selectedRoom.chatRoomId)} + onClose={closeModal} + webSocketService={webSocketService} isAdminPage={true} /> )} diff --git a/src/components/pages/community/api/chatApi.ts b/src/components/pages/community/api/chatApi.ts index abe3979..a333ba7 100644 --- a/src/components/pages/community/api/chatApi.ts +++ b/src/components/pages/community/api/chatApi.ts @@ -1,81 +1,60 @@ -// import axiosInstance from '../../../../api/axiosInstance'; // 기존 Axios 인스턴스 사용 -import { - mockFetchChatRooms, - mockFetchChatMessages, - mockFetchChatRoomDetails, - mockSendMessage, - mockDeleteChatRoom, -} from '../../../../mocks/chatData'; +import axiosInstance from '../../../../api/axiosInstance'; +import { webSocketService } from '../../../../utils/webSocket'; /** - * 채팅방 목록 가져오기 + * 채팅방 생성 + * @param postId 게시물 ID + * @returns 생성된 채팅방 정보 + */ +export const createChatRoom = async ( + postId: number +): Promise<{ + id: number; + roomName: string; + createdAt: string | null; + capacity: number; +}> => { + try { + const response = await axiosInstance.post('/chat', { postId }); + return response.data; + } catch (error) { + console.error('채팅방 생성 오류:', error); + throw new Error('채팅방을 생성할 수 없습니다.'); + } +}; + +/** + * 관리자 페이지 내 채팅방 목록 가져오기 * @returns 채팅방 목록 배열 */ export const fetchChatRooms = async (): Promise< { - chatRoomId: string; - chatRoomTitle: string; - participants: { userId: string; nickname: string }[]; + postId: number; + capacity: number; + roomName: string; + chatMembers: string[]; }[] > => { try { // 실제 API 사용 시 - // const response = await axiosInstance.get('/chat'); // API 엔드포인트 수정 필요 - // return response.data; - - // Mock 데이터 기반: - return mockFetchChatRooms(); + const response = await axiosInstance.get('/admin/chatlist'); + return response.data; } catch (error) { console.error('채팅방 목록 조회 오류:', error); throw new Error('채팅방 목록을 불러올 수 없습니다.'); } }; -/** - * 채팅방 세부 정보 조회 - * @param chatRoomId 채팅방 ID - * @returns 채팅방 세부 정보 (참여자 정보, 작성자 닉네임, 채팅방 제목 포함) - */ - -export const fetchChatRoomDetails = async ( - chatRoomId: string -): Promise<{ - chatRoomId: string; - participants: { userId: string; nickname: string }[]; - authorNickname: string; - chatRoomTitle: string; -}> => { - try { - // 실제 API 사용 - // const response = await axiosInstance.get(`/chat/${chatRoomId}`); - // return { - // chatRoomId, - // participants: response.data.participants, - // authorNickname: response.data.authorNickname, - // chatRoomTitle: response.data.chatRoomTitle, - // }; - - // Mock 데이터 기반: - return mockFetchChatRoomDetails(chatRoomId); - } catch (error) { - console.error('채팅방 정보 조회 오류:', error); - throw new Error('채팅방 정보를 불러올 수 없습니다.'); - } -}; - /** * 채팅 메시지 가져오기 - * @param chatRoomId 채팅방 ID + * @param id 채팅방 ID * @returns 채팅 메시지 리스트 */ -export const fetchChatMessages = async (chatRoomId: string) => { +export const fetchChatMessages = async (id: number) => { try { // 실제 API 사용 - // const response = await axiosInstance.get(`/chat/${chatRoomId}/messages`); - // return response.data; - - // Mock 데이터 기반: - return mockFetchChatMessages(chatRoomId); + const response = await axiosInstance.get(`/chat/${id}/messages`); + return response.data; } catch (error) { console.error('채팅 메시지 조회 오류:', error); throw new Error('채팅 메시지를 조회할 수 없습니다.'); @@ -84,7 +63,7 @@ export const fetchChatMessages = async (chatRoomId: string) => { /** * 채팅 메시지 전송 - * @param chatRoomId 채팅방 ID + * @param id 채팅방 ID * @param senderId 메시지 송신자 ID * @param content 메시지 내용 */ @@ -95,11 +74,11 @@ export const sendMessage = async ( ): Promise => { try { // 실제 WebSocket 발행 메시지 전송 - // const message = { senderId, content }; - // socket.send(`/pub/message/${chatRoomId}`, JSON.stringify(message)); - - // Mock API 사용 - await mockSendMessage(chatRoomId, senderId, content); + const message = { senderId, content }; + webSocketService.send( + `/pub/message/${chatRoomId}`, + JSON.stringify(message) + ); } catch (error) { console.error('채팅 메시지 전송 오류:', error); throw new Error('채팅 메시지를 전송할 수 없습니다.'); @@ -108,16 +87,13 @@ export const sendMessage = async ( /** * 채팅방 삭제 - * @param chatRoomId 삭제할 채팅방 ID + * @param id 삭제할 채팅방 ID */ -export const deleteChatRoom = async (chatRoomId: string): Promise => { +export const deleteChatRoom = async (id: number): Promise => { try { // 실제 API 사용 - // const response = await axiosInstance.delete(`/chat/${chatRoomId}`); - // return response.data; - - // Mock 데이터 기반: - await mockDeleteChatRoom(chatRoomId); + const response = await axiosInstance.delete(`/chat/${id}`); + return response.data; } catch (error) { console.error('채팅방 삭제 오류:', error); throw new Error('채팅방을 삭제할 수 없습니다.'); diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index e4d5430..4ccae6b 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export class WebSocketService { private socket: WebSocket | null = null; + private subscriptions: Map void> = new Map(); // 구독 채널 관리 + private reconnectSubscriptions: Set = new Set(); // 재구독이 필요한 채널 관리 private reconnectAttempts = 0; private maxReconnectAttempts = 5; @@ -18,11 +20,18 @@ export class WebSocketService { console.log('WebSocket connected'); if (onOpen) onOpen(); this.reconnectAttempts = 0; // 성공적으로 연결되면 재시도 횟수 초기화 + this.resubscribe(); // 재연결 시 특정 구독 복구 }; this.socket.onmessage = (event) => { const data = JSON.parse(event.data); - if (onMessage) onMessage(data); + const destination = data.destination; + if (destination && this.subscriptions.has(destination)) { + const callback = this.subscriptions.get(destination); + if (callback) callback(data.message); + } else { + console.warn('No subscription found for destination:', destination); + } }; this.socket.onclose = () => { @@ -37,6 +46,33 @@ export class WebSocketService { }; } + subscribe( + destination: string, + callback: (data: any) => void, + enableReconnect = false + ) { + if (!this.subscriptions.has(destination)) { + this.subscriptions.set(destination, callback); + console.log(`Subscribed to ${destination}`); + } else { + console.warn(`Already subscribed to ${destination}`); + } + // 재구독이 필요한 경우에만 추가 + if (enableReconnect) { + this.reconnectSubscriptions.add(destination); + } + } + + unsubscribe(destination: string) { + if (this.subscriptions.has(destination)) { + this.subscriptions.delete(destination); + this.reconnectSubscriptions.delete(destination); // 재구독 목록에서도 제거 + console.log(`Unsubscribed from ${destination}`); + } else { + console.warn(`No subscription found for ${destination}`); + } + } + send(destination: string, message: any) { if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { console.warn('WebSocket is not connected'); @@ -48,6 +84,10 @@ export class WebSocketService { this.socket.send(payload); } + isConnected(): boolean { + return this.socket?.readyState === WebSocket.OPEN; + } + close() { if (this.socket) { this.socket.close(); @@ -63,16 +103,29 @@ export class WebSocketService { ); setTimeout(() => { this.connect( - () => {}, - () => {}, - () => {}, - () => {} + (data) => console.log('Reconnected message:', data), // onMessage + () => console.log('Reconnected successfully'), // onOpen + () => console.log('Reconnection closed'), // onClose + (error) => console.error('Reconnection error:', error) // onError ); }, 3000); // 3초 후 재연결 시도 } else { console.error('Max reconnect attempts reached'); } } + + private resubscribe() { + if (this.reconnectSubscriptions.size > 0) { + console.log('Resubscribing to specific channels after reconnect'); + this.reconnectSubscriptions.forEach((destination) => { + const callback = this.subscriptions.get(destination); + if (callback) { + console.log(`Resubscribing to ${destination}`); + this.subscribe(destination, callback); // 구독 복구 + } + }); + } + } } export const webSocketService = new WebSocketService(