import { useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'; import { mergeRecoveredChatMessages } from '../../mainChatPanel/chatUtils'; import { chatGateway } from '../data/chatGateway'; import type { ChatConversationRequest, ChatConversationSummary, ChatMessage, } from '../../mainChatPanel/types'; const INITIAL_CONVERSATION_MESSAGE_LIMIT = 3; const OLDER_CONVERSATION_MESSAGE_PAGE_SIZE = 20; function getCachedSessionMessages(cache: Map, sessionId: string) { const normalizedSessionId = sessionId.trim(); if (!normalizedSessionId) { return [] as ChatMessage[]; } return cache.get(normalizedSessionId) ?? []; } function getBestAvailableSessionMessages( cache: Map, sessionId: string, currentSessionId: string, currentMessages: ChatMessage[], ) { const cachedMessages = getCachedSessionMessages(cache, sessionId); if (sessionId !== currentSessionId || currentMessages.length === 0) { return cachedMessages; } return mergeRecoveredChatMessages(cachedMessages, currentMessages); } type UseConversationRoomDataOptions = { activeSessionId: string; connectionState: 'connecting' | 'connected' | 'disconnected'; shouldBlockConversationWhileLoading: (sessionId: string) => boolean; captureViewportRestoreSnapshot: () => void; sessionMessageCacheRef: MutableRefObject>; messagesRef: MutableRefObject; pendingViewportRestoreRef: MutableRefObject; shouldRestoreConversationAfterReconnectRef: MutableRefObject; setConversationItems: Dispatch>; setMessages: Dispatch>; setRequestItems: Dispatch>; setConversationLoadingLabel: Dispatch>; setIsConversationContentLoading: Dispatch>; setIsDeferringAuxiliaryChatRequests: Dispatch>; setHasOlderMessages: Dispatch>; setOldestLoadedMessageId: Dispatch>; setIsLoadingOlderMessages: Dispatch>; queueViewportPrependRestore: (previousScrollHeight: number, previousScrollTop: number) => void; viewportRef: MutableRefObject; }; export function useConversationRoomData({ activeSessionId, connectionState, shouldBlockConversationWhileLoading, captureViewportRestoreSnapshot, sessionMessageCacheRef, messagesRef, pendingViewportRestoreRef, shouldRestoreConversationAfterReconnectRef, setConversationItems, setMessages, setRequestItems, setConversationLoadingLabel, setIsConversationContentLoading, setIsDeferringAuxiliaryChatRequests, setHasOlderMessages, setOldestLoadedMessageId, setIsLoadingOlderMessages, queueViewportPrependRestore, viewportRef, }: UseConversationRoomDataOptions) { useEffect(() => { if (!activeSessionId.trim()) { setMessages([]); setRequestItems([]); setIsConversationContentLoading(false); setIsLoadingOlderMessages(false); setHasOlderMessages(false); setOldestLoadedMessageId(null); return; } let isCancelled = false; const requestedSessionId = activeSessionId; const loadConversationDetail = async () => { captureViewportRestoreSnapshot(); pendingViewportRestoreRef.current = true; setConversationLoadingLabel('대화 내용을 불러오는 중입니다.'); setIsConversationContentLoading(shouldBlockConversationWhileLoading(requestedSessionId)); setIsDeferringAuxiliaryChatRequests(true); try { const response = await chatGateway.getConversationDetail(requestedSessionId, { limit: INITIAL_CONVERSATION_MESSAGE_LIMIT, }); if (!isCancelled && response.item.sessionId === requestedSessionId) { setConversationItems((previous) => { const exists = previous.some((item) => item.sessionId === response.item.sessionId); if (!exists) { return [response.item, ...previous]; } return previous.map((item) => (item.sessionId === response.item.sessionId ? response.item : item)); }); const baseMessages = getBestAvailableSessionMessages( sessionMessageCacheRef.current, requestedSessionId, activeSessionId, messagesRef.current, ); const nextMessages = mergeRecoveredChatMessages(baseMessages, response.messages); sessionMessageCacheRef.current.set(requestedSessionId, nextMessages); setMessages(nextMessages); setRequestItems(response.requests); setHasOlderMessages(response.hasOlderMessages); setOldestLoadedMessageId(response.oldestLoadedMessageId); } } catch { if (!isCancelled) { const cachedMessages = getBestAvailableSessionMessages( sessionMessageCacheRef.current, requestedSessionId, activeSessionId, messagesRef.current, ); if (cachedMessages.length > 0) { setMessages(cachedMessages); } } } finally { if (!isCancelled) { setIsConversationContentLoading(false); setIsDeferringAuxiliaryChatRequests(false); } } }; void loadConversationDetail(); return () => { isCancelled = true; }; }, [ activeSessionId, captureViewportRestoreSnapshot, messagesRef, pendingViewportRestoreRef, sessionMessageCacheRef, setConversationItems, setConversationLoadingLabel, setIsConversationContentLoading, setIsDeferringAuxiliaryChatRequests, setHasOlderMessages, setMessages, setOldestLoadedMessageId, setRequestItems, shouldBlockConversationWhileLoading, ]); useEffect(() => { if (connectionState !== 'connected' || !shouldRestoreConversationAfterReconnectRef.current) { return; } let isCancelled = false; const requestedSessionId = activeSessionId; const restoreConversationAfterReconnect = async () => { setIsDeferringAuxiliaryChatRequests(true); try { const response = await chatGateway.getConversationDetail(requestedSessionId, { limit: Math.max(INITIAL_CONVERSATION_MESSAGE_LIMIT, messagesRef.current.length || 0), }); if (!isCancelled && response.item.sessionId === requestedSessionId) { const baseMessages = getBestAvailableSessionMessages( sessionMessageCacheRef.current, requestedSessionId, activeSessionId, messagesRef.current, ); const nextMessages = mergeRecoveredChatMessages(baseMessages, response.messages); const hasMessageDiff = nextMessages !== baseMessages; if (hasMessageDiff) { captureViewportRestoreSnapshot(); pendingViewportRestoreRef.current = true; setConversationLoadingLabel('채팅방을 다시 연결하고 내용을 복구하는 중입니다.'); setIsConversationContentLoading(true); } setConversationItems((previous) => { const exists = previous.some((item) => item.sessionId === response.item.sessionId); if (!exists) { return [response.item, ...previous]; } return previous.map((item) => (item.sessionId === response.item.sessionId ? response.item : item)); }); setRequestItems(response.requests); setHasOlderMessages(response.hasOlderMessages); setOldestLoadedMessageId(response.oldestLoadedMessageId); if (hasMessageDiff) { sessionMessageCacheRef.current.set(requestedSessionId, nextMessages); setMessages(nextMessages); window.requestAnimationFrame(() => { if (!isCancelled) { setIsConversationContentLoading(false); } }); } } } catch { if (!isCancelled) { setIsConversationContentLoading(false); } } finally { if (!isCancelled) { shouldRestoreConversationAfterReconnectRef.current = false; if (!pendingViewportRestoreRef.current) { setIsConversationContentLoading(false); } setIsDeferringAuxiliaryChatRequests(false); } } }; void restoreConversationAfterReconnect(); return () => { isCancelled = true; }; }, [ activeSessionId, captureViewportRestoreSnapshot, connectionState, messagesRef, pendingViewportRestoreRef, sessionMessageCacheRef, setConversationItems, setConversationLoadingLabel, setIsConversationContentLoading, setIsDeferringAuxiliaryChatRequests, setHasOlderMessages, setMessages, setOldestLoadedMessageId, setRequestItems, shouldRestoreConversationAfterReconnectRef, ]); const loadOlderMessages = async () => { const requestedSessionId = activeSessionId.trim(); const oldestVisibleMessageId = messagesRef.current[0]?.id ?? null; if (!requestedSessionId || oldestVisibleMessageId == null) { return; } setIsLoadingOlderMessages(true); try { const response = await chatGateway.getConversationDetail(requestedSessionId, { limit: OLDER_CONVERSATION_MESSAGE_PAGE_SIZE, beforeMessageId: oldestVisibleMessageId, }); if (response.item.sessionId !== requestedSessionId || response.messages.length === 0) { setHasOlderMessages(response.hasOlderMessages); setOldestLoadedMessageId(response.oldestLoadedMessageId); return; } const viewport = viewportRef.current; const previousScrollHeight = viewport?.scrollHeight ?? 0; const previousScrollTop = viewport?.scrollTop ?? 0; const nextMessages = mergeRecoveredChatMessages(messagesRef.current, response.messages); queueViewportPrependRestore(previousScrollHeight, previousScrollTop); sessionMessageCacheRef.current.set(requestedSessionId, nextMessages); setMessages(nextMessages); setRequestItems(response.requests); setHasOlderMessages(response.hasOlderMessages); setOldestLoadedMessageId(response.oldestLoadedMessageId); } finally { setIsLoadingOlderMessages(false); } }; return { loadOlderMessages, }; }