[RAPID-fix] 메신저 사용자 목록 회사 전환 시 캐시 격리 - useRooms/useCompanyUsers queryKey에 companyCode 포함 - 회사 전환 시 다른 회사 사용자가 캐시에서 노출되던 문제 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> [RAPID-fix] 메신저 버그 수정 (8건) - 방 생성 후 자동 입장 + 커서 포커스 - DM 헤더 상대방 이름, 그룹 "이름1, 이름2 외 N명" 표시 - 채팅방 이름 인라인 수정 기능 추가 - Socket.IO join_rooms 누락 수정 → 실시간 메시지 수신 정상화 - new_message 이벤트 수신 시 React Query 캐시 무효화 - 토스트 알림 stale closure 수정 (ref 패턴 적용) - 타이핑 이벤트명 백엔드 일치 (user_typing/user_stop_typing) - 메시지 순서 역전 수정 (.reverse()) - unread queryKey 불일치 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> [RAPID-fix] REST API 메시지 전송 시 Socket.IO broadcast 추가 - socketManager.ts 모듈 생성 (io 전역 공유) - sendMessage 컨트롤러에서 io.to(room).emit('new_message') broadcast - 상대방 말풍선 너비 고정 수정 (items-start 추가) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useCallback, useState } from "react";
|
|
import { io, Socket } from "socket.io-client";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { useMessengerContext } from "@/contexts/MessengerContext";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080";
|
|
|
|
interface NewMessageEvent {
|
|
room_id: number;
|
|
sender_name: string;
|
|
sender_id: string;
|
|
content: string;
|
|
}
|
|
|
|
interface TypingEvent {
|
|
room_id: number;
|
|
user_id: string;
|
|
user_name: string;
|
|
}
|
|
|
|
export function useMessengerSocket() {
|
|
const socketRef = useRef<Socket | null>(null);
|
|
const { selectedRoomId, notificationEnabled } = useMessengerContext();
|
|
const selectedRoomIdRef = useRef(selectedRoomId);
|
|
const notificationEnabledRef = useRef(notificationEnabled);
|
|
const { toast } = useToast();
|
|
const qc = useQueryClient();
|
|
const [onlineUsers, setOnlineUsers] = useState<Set<string>>(new Set());
|
|
const [typingUsers, setTypingUsers] = useState<Map<string, string[]>>(new Map());
|
|
|
|
// Keep refs in sync so socket handlers use latest values
|
|
useEffect(() => {
|
|
selectedRoomIdRef.current = selectedRoomId;
|
|
}, [selectedRoomId]);
|
|
|
|
useEffect(() => {
|
|
notificationEnabledRef.current = notificationEnabled;
|
|
}, [notificationEnabled]);
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem("authToken");
|
|
if (!token) return;
|
|
|
|
const socket = io(BACKEND_URL, {
|
|
path: "/socket.io",
|
|
auth: { token },
|
|
transports: ["websocket", "polling"],
|
|
});
|
|
|
|
socketRef.current = socket;
|
|
|
|
// BUG-8: Join rooms on connect
|
|
socket.on("connect", () => {
|
|
socket.emit("join_rooms");
|
|
});
|
|
|
|
socket.on("user_online", (data: { userId: string; online: boolean }) => {
|
|
setOnlineUsers((prev) => {
|
|
const next = new Set(prev);
|
|
if (data.online) next.add(data.userId);
|
|
else next.delete(data.userId);
|
|
return next;
|
|
});
|
|
});
|
|
|
|
// BUG-5 & BUG-8: Handle new_message with cache invalidation and toast
|
|
socket.on("new_message", (data: NewMessageEvent) => {
|
|
const roomIdStr = String(data.room_id);
|
|
|
|
// Invalidate React Query caches
|
|
qc.invalidateQueries({ queryKey: ["messenger", "messages", roomIdStr] });
|
|
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
|
|
qc.invalidateQueries({ queryKey: ["messenger", "unread"] });
|
|
|
|
// Toast for messages in other rooms
|
|
if (roomIdStr !== selectedRoomIdRef.current && notificationEnabledRef.current) {
|
|
toast({
|
|
title: data.sender_name,
|
|
description: data.content?.slice(0, 50),
|
|
});
|
|
}
|
|
});
|
|
|
|
// BUG-7: Backend emits "user_typing" / "user_stop_typing", not "typing_start" / "typing_stop"
|
|
socket.on("user_typing", (data: TypingEvent) => {
|
|
const roomIdStr = String(data.room_id);
|
|
setTypingUsers((prev) => {
|
|
const next = new Map(prev);
|
|
const users = next.get(roomIdStr) || [];
|
|
if (!users.includes(data.user_name)) {
|
|
next.set(roomIdStr, [...users, data.user_name]);
|
|
}
|
|
return next;
|
|
});
|
|
});
|
|
|
|
socket.on("user_stop_typing", (data: TypingEvent) => {
|
|
const roomIdStr = String(data.room_id);
|
|
setTypingUsers((prev) => {
|
|
const next = new Map(prev);
|
|
const users = next.get(roomIdStr) || [];
|
|
next.set(
|
|
roomIdStr,
|
|
users.filter((u) => u !== data.user_name)
|
|
);
|
|
return next;
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
socket.disconnect();
|
|
socketRef.current = null;
|
|
};
|
|
}, [toast, qc]);
|
|
|
|
// BUG-7: Backend expects { room_id }, not { roomId }
|
|
const emitTypingStart = useCallback(
|
|
(roomId: string) => {
|
|
socketRef.current?.emit("typing_start", { room_id: Number(roomId) });
|
|
},
|
|
[]
|
|
);
|
|
|
|
const emitTypingStop = useCallback(
|
|
(roomId: string) => {
|
|
socketRef.current?.emit("typing_stop", { room_id: Number(roomId) });
|
|
},
|
|
[]
|
|
);
|
|
|
|
return { socket: socketRef, onlineUsers, typingUsers, emitTypingStart, emitTypingStop };
|
|
}
|