diff --git a/src/api/chatApi.js b/src/api/chatApi.js index 1ca7459..642de59 100644 --- a/src/api/chatApi.js +++ b/src/api/chatApi.js @@ -1,38 +1,50 @@ import api from './client'; +// 공통 API 호출 함수 +const apiCall = (method, url, data = null, params = null) => { + return api({ + method, + url, + data, + params, + }); +}; + // --- ChatRoomController API --- // 모든 채팅방 목록 가져오기 -export const fetchChatRooms = () => api.get('/chat-room/all'); +export const fetchChatRooms = () => apiCall('get', '/chat-room/all'); // 특정 채팅방의 메시지 내역 가져오기 (페이지네이션) export const fetchMessages = (chatRoomId, page = 0, size = 50) => - api.get(`/chat-room/${chatRoomId}/messages`, { params: { page, size } }); + apiCall('get', `/chat-room/${chatRoomId}/messages`, null, { page, size }); // 채팅방 생성 export const createChatRoom = (memberIdList, roomName, type) => - api.post('/chat-room/create', { memberIdList, name: roomName, type }); + apiCall('post', '/chat-room/create', { memberIdList, name: roomName, type }); // 채팅방에 사용자 초대 export const inviteToChatRoom = (chatRoomId, memberIdList) => - api.post(`/chat-room/${chatRoomId}/invite`, { memberIdList }); + apiCall('post', `/chat-room/${chatRoomId}/invite`, { memberIdList }); // 이미 존재하는 채팅방에 멤버 추가 export const addMembersToExistingChatRoom = (chatRoomId, memberIdList) => - api.post(`/chat-room/${chatRoomId}/add-members`, { chatRoomId, memberIdList }); + apiCall('post', `/chat-room/${chatRoomId}/add-members`, { chatRoomId, memberIdList }); // 메시지 삭제 -export const deleteMessage = (messageId) => api.delete(`/chat-room/messages/${messageId}`); +export const deleteMessage = (messageId) => + apiCall('delete', `/chat-room/messages/${messageId}`); // 채팅방 퇴장 -export const leaveChatRoom = (chatRoomId) => api.delete(`/chat-room/delete/${chatRoomId}`); +export const leaveChatRoom = (chatRoomId) => + apiCall('delete', `/chat-room/delete/${chatRoomId}`); // 전체 안 읽은 메시지 개수 조회 -export const fetchTotalUnreadCount = () => api.get('/chat-room/total-unread-count'); +export const fetchTotalUnreadCount = () => apiCall('get', '/chat-room/total-unread-count'); // 특정 채팅방 메시지 모두 읽음 처리 export const markAllAsRead = (chatRoomId, lastReadMessageId) => - api.get(`/chat-room/${chatRoomId}/mark-all-as-read`, { params: { lastReadMessageId } }); + apiCall('get', `/chat-room/${chatRoomId}/mark-all-as-read`, null, { lastReadMessageId }); // --- MemberProfileController API --- diff --git a/src/api/socketService.js b/src/api/socketService.js index 3a5c5e0..13abeb0 100644 --- a/src/api/socketService.js +++ b/src/api/socketService.js @@ -40,6 +40,10 @@ const performSubscribe = (destination, callback) => { return subscription; }; +const BASE_URL = window.location.hostname === "localhost" + ? "http://localhost:8081/ws/chat" + : "https://api.sysonetaskmanager.store/ws/chat"; + // WebSocket 연결 export const connectWebSocket = (onConnected) => { if (stompClient && stompClient.active) { @@ -50,7 +54,7 @@ export const connectWebSocket = (onConnected) => { } stompClient = new Client({ - webSocketFactory: () => new SockJS('http://localhost:8081/ws/chat'), // 백엔드 포트 8081로 수정 + webSocketFactory: () => new SockJS(BASE_URL), reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, @@ -85,22 +89,40 @@ export const disconnectWebSocket = () => { } }; -export const subscribe = (destination, callback) => { - return new Promise((resolve, reject) => { - if (isConnected && stompClient && stompClient.active) { - // 이미 연결되어 있으면 바로 구독 - try { - const subscription = performSubscribe(destination, callback); - resolve(subscription); - } catch (error) { - reject(error); - } - } else { - // 연결이 안 되어 있으면 큐에 저장 - console.warn('STOMP client is not connected. Queueing subscription for ', destination); - subscriptionQueue.push({ destination, callback, resolve, reject }); - } +// 공통 구독/해제 유틸 함수 +const manageSubscription = (action, destination, callback) => { + if (action === 'subscribe') { + if (subscriptions.has(destination)) { + subscriptions.get(destination).unsubscribe(); + subscriptions.delete(destination); + } + const subscription = stompClient.subscribe(destination, (message) => { + callback(JSON.parse(message.body)); }); + subscriptions.set(destination, subscription); + return subscription; + } else if (action === 'unsubscribe') { + if (subscriptions.has(destination)) { + subscriptions.get(destination).unsubscribe(); + subscriptions.delete(destination); + } + } +}; + +export const subscribe = (destination, callback) => { + return new Promise((resolve, reject) => { + if (isConnected && stompClient && stompClient.active) { + try { + const subscription = manageSubscription('subscribe', destination, callback); + resolve(subscription); + } catch (error) { + reject(error); + } + } else { + console.warn('STOMP client is not connected. Queueing subscription for ', destination); + subscriptionQueue.push({ destination, callback, resolve, reject }); + } + }); } // 특정 채팅방 토픽 구독 @@ -115,10 +137,7 @@ export const subscribeToUserQueue = (queueName, onNotificationReceived) => { // 구독 해지 export const unsubscribe = (destination) => { - if (subscriptions.has(destination)) { - subscriptions.get(destination).unsubscribe(); - subscriptions.delete(destination); - } + manageSubscription('unsubscribe', destination); } // 메시지 전송 (발행) @@ -131,4 +150,20 @@ export const sendMessage = (chatMessage) => { } else { console.error('Cannot send message, STOMP client is not connected.'); } +}; + +// 초대 메시지 전송 +export const sendInviteMessage = (chatRoomId, memberId, memberIdList) => { + if (stompClient && stompClient.active) { + stompClient.publish({ + destination: '/app/invite', + body: JSON.stringify({ + chatRoomId, + memberId, + memberIdList, + }), + }); + } else { + console.error('Cannot send invite message, STOMP client is not connected.'); + } }; \ No newline at end of file diff --git a/src/components/common/chat/ChatRoom.jsx b/src/components/common/chat/ChatRoom.jsx index 1ba3365..ae51476 100644 --- a/src/components/common/chat/ChatRoom.jsx +++ b/src/components/common/chat/ChatRoom.jsx @@ -1,6 +1,9 @@ -import { useEffect, useRef} from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Send } from '@mui/icons-material'; import useChatStore from '../../../store/chatStore'; +import { sendInviteMessage } from '../../../api/socketService'; +import { useDebounce } from '../../../hooks/useDebounce'; +import * as chatApi from '../../../api/chatApi'; const ChatRoom = ({ selectedChat, @@ -9,6 +12,12 @@ const ChatRoom = ({ onSendMessage }) => { const messagesEndRef = useRef(null); + const [inviteModalOpen, setInviteModalOpen] = useState(false); + const [employeeSearchTerm, setEmployeeSearchTerm] = useState(''); + const [allEmployees, setAllEmployees] = useState([]); + const [selectedEmployees, setSelectedEmployees] = useState([]); + const [inviteSuccessMessage, setInviteSuccessMessage] = useState(''); + const debouncedSearchTerm = useDebounce(employeeSearchTerm, 500); const { getMessages, connectWebSocket, disconnectWebSocket, markAsRead, currentUser } = useChatStore(); const messages = [...getMessages(selectedChat?.id || 0)].sort((a, b) => { @@ -46,8 +55,97 @@ const ChatRoom = ({ }); }; + const handleToggleEmployee = (employee) => { + setSelectedEmployees((prev) => + prev.some((e) => e.id === employee.id) + ? prev.filter((e) => e.id !== employee.id) + : [...prev, employee] + ); + }; + + const handleInviteMembers = async () => { + if (selectedEmployees.length === 0) { + alert('초대할 사원을 선택하세요.'); + return; + } + + try { + sendInviteMessage( + selectedChat.id, + currentUser.id, + selectedEmployees.map((e) => e.id) + ); + setInviteSuccessMessage('초대가 완료되었습니다.'); + setTimeout(() => setInviteSuccessMessage(''), 3000); // 3초 후 메시지 제거 + setInviteModalOpen(false); + } catch (error) { + console.error('Failed to invite members:', error); + alert('초대에 실패했습니다. 다시 시도해주세요.'); + } + }; + + useEffect(() => { + const fetchEmployees = async () => { + try { + const response = debouncedSearchTerm + ? await chatApi.searchMembers(debouncedSearchTerm) + : await chatApi.fetchAllMembers(); + setAllEmployees(response.data.data); + } catch (error) { + console.error('Failed to fetch employees:', error); + } + }; + + fetchEmployees(); + }, [debouncedSearchTerm]); + + // 메시지 렌더링 컴포넌트 분리 + const Message = ({ msg, isMine, formatTime }) => ( +
{msg.senderName}
+ )} + {msg.type === 'DELETED' ? ( +삭제된 메시지입니다.
+ ) : ( +{msg.content}
+ )} +{msg.senderName}
- )} - {msg.type === 'DELETED' ? ( -삭제된 메시지입니다.
- ) : ( -{msg.content}
- )} -