Files
vexplor_dev/frontend/hooks/useMessenger.ts
syc0123 0be75f2f41 [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>
2026-03-31 09:36:07 +09:00

220 lines
6.6 KiB
TypeScript

"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
// ============================================
// Types
// ============================================
export interface Room {
id: string;
name: string;
type: "dm" | "group" | "channel";
lastMessage?: string;
lastMessageAt?: string;
unreadCount: number;
participants: Participant[];
description?: string;
}
export interface Participant {
userId: string;
userName: string;
photo?: string | null;
}
export interface Message {
id: string;
roomId: string;
senderId: string;
senderName: string;
senderPhoto?: string | null;
content: string;
type: "text" | "file" | "system";
fileUrl?: string;
fileName?: string;
reactions: Reaction[];
threadCount?: number;
parentId?: string | null;
isDeleted: boolean;
createdAt: string;
}
export interface Reaction {
emoji: string;
users: { userId: string; userName: string }[];
}
export interface CompanyUser {
userId: string;
userName: string;
deptName?: string;
positionName?: string;
photo?: string | null;
}
// ============================================
// API helpers
// ============================================
async function fetchApi<T>(url: string): Promise<T> {
const res = await apiClient.get(url);
return res.data?.data ?? res.data;
}
async function postApi<T>(url: string, data?: unknown): Promise<T> {
const res = await apiClient.post(url, data);
return res.data?.data ?? res.data;
}
// ============================================
// Hooks
// ============================================
export function useRooms() {
const { user } = useAuth();
const companyCode = user?.companyCode || user?.company_code;
return useQuery<Room[]>({
queryKey: ["messenger", "rooms", companyCode],
queryFn: async () => {
const data = await fetchApi<any[]>("/messenger/rooms");
return data.map((r) => ({
id: String(r.id),
name: r.room_name ?? r.name ?? "",
type: r.room_type ?? r.type,
lastMessage: r.last_message ?? r.lastMessage,
lastMessageAt: r.last_message_at ?? r.lastMessageAt,
unreadCount: r.unread_count ?? r.unreadCount ?? 0,
description: r.description,
participants: (r.participants ?? []).map((p: any) => ({
userId: p.user_id ?? p.userId,
userName: p.user_name ?? p.userName,
photo: p.photo ? `data:image/jpeg;base64,${p.photo}` : null,
})),
}));
},
refetchInterval: 30000,
});
}
export function useMessages(roomId: string | null) {
return useQuery<Message[]>({
queryKey: ["messenger", "messages", roomId],
queryFn: async () => {
const data = await fetchApi<any[]>(`/messenger/rooms/${roomId}/messages`);
return data.map((m) => ({
id: String(m.id),
roomId: String(m.room_id ?? m.roomId),
senderId: m.sender_id ?? m.senderId,
senderName: m.sender_name ?? m.senderName ?? m.sender_id,
senderPhoto: m.sender_photo
? `data:image/jpeg;base64,${m.sender_photo}`
: (m.senderPhoto ?? null),
content: m.content ?? "",
type: m.message_type ?? m.type ?? "text",
fileUrl: m.file_url ?? m.fileUrl,
fileName: m.file_name ?? m.fileName,
reactions: m.reactions ?? [],
threadCount: m.thread_count ?? m.threadCount ?? 0,
parentId: m.parent_message_id ?? m.parentId ?? null,
isDeleted: m.is_deleted ?? m.isDeleted ?? false,
createdAt: m.created_at ?? m.createdAt,
}));
},
enabled: !!roomId,
});
}
export function useCompanyUsers() {
const { user } = useAuth();
const companyCode = user?.companyCode || user?.company_code;
return useQuery<CompanyUser[]>({
queryKey: ["messenger", "users", companyCode],
queryFn: async () => {
const data = await fetchApi<any[]>("/messenger/users");
return data.map((u) => ({
userId: u.user_id ?? u.userId,
userName: u.user_name ?? u.userName,
deptName: u.dept_name ?? u.deptName,
photo: u.photo ? `data:image/jpeg;base64,${u.photo}` : null,
}));
},
});
}
export function useUnreadCount() {
return useQuery<number>({
queryKey: ["messenger", "unread"],
queryFn: () => fetchApi("/messenger/unread"),
refetchInterval: 15000,
});
}
export function useSendMessage() {
const qc = useQueryClient();
return useMutation({
mutationFn: (payload: { roomId: string; content: string; type?: string; parentId?: string | null }) =>
postApi(`/messenger/rooms/${payload.roomId}/messages`, payload),
onSuccess: (_data, variables) => {
qc.invalidateQueries({ queryKey: ["messenger", "messages", variables.roomId] });
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
},
});
}
export function useCreateRoom() {
const qc = useQueryClient();
return useMutation({
mutationFn: (payload: { type: "dm" | "group" | "channel"; name?: string; description?: string; participantIds: string[] }) =>
postApi<Room>("/messenger/rooms", payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
},
});
}
export function useMarkAsRead() {
const qc = useQueryClient();
return useMutation({
mutationFn: (roomId: string) => postApi(`/messenger/rooms/${roomId}/read`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["messenger", "unread"] });
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
},
});
}
export function useAddReaction() {
const qc = useQueryClient();
return useMutation({
mutationFn: (payload: { messageId: string; roomId: string; emoji: string }) =>
postApi(`/messenger/messages/${payload.messageId}/reactions`, { emoji: payload.emoji }),
onSuccess: (_data, variables) => {
qc.invalidateQueries({ queryKey: ["messenger", "messages", variables.roomId] });
},
});
}
export function useUpdateRoom() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ roomId, name }: { roomId: string; name: string }) =>
apiClient.put(`/messenger/rooms/${roomId}`, { room_name: name }).then((res) => res.data?.data ?? res.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
},
});
}
export function useUploadFile() {
return useMutation({
mutationFn: async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const res = await apiClient.post("/messenger/files/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return res.data?.data ?? res.data;
},
});
}