From 3aee3133ba422297aeb198be2cc41ea061b706d4 Mon Sep 17 00:00:00 2001 From: manNomi Date: Fri, 27 Mar 2026 19:18:15 +0900 Subject: [PATCH] =?UTF-8?q?fix(web):=20=EC=B1=84=ED=8C=85=20=EC=9E=AC?= =?UTF-8?q?=EC=A7=84=EC=9E=85=20=EC=8B=9C=20=ED=9E=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9C=A0=EC=8B=A4=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/apis/chat/normalize.ts | 41 +++++++++++++++---- .../ChatContent/_hooks/useChatListHandler.ts | 19 +++++++-- .../chat/[chatId]/_ui/ChatContent/index.tsx | 7 +++- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/apps/web/src/apis/chat/normalize.ts b/apps/web/src/apis/chat/normalize.ts index 8850d708..58298bff 100644 --- a/apps/web/src/apis/chat/normalize.ts +++ b/apps/web/src/apis/chat/normalize.ts @@ -48,6 +48,28 @@ const toNumber = (value: NumericLike): number => { return 0; }; +const createStableHash = (value: string): number => { + let hash = 0; + + for (let i = 0; i < value.length; i++) { + hash = (hash * 31 + value.charCodeAt(i)) | 0; + } + + const normalized = Math.abs(hash); + return normalized === 0 ? 1 : normalized; +}; + +const getFallbackMessageId = (message: RawChatMessage): number => { + const senderId = toNumber(message.senderId ?? message.siteUserId); + const attachmentSignature = (message.attachments ?? []) + .map((attachment) => `${attachment.isImage ? "image" : "file"}:${attachment.url}:${attachment.createdAt}`) + .join(","); + const seed = `${senderId}|${message.createdAt}|${message.content}|${attachmentSignature}`; + + // 서버에서 id가 누락되는 경우를 대비해 항상 동일한 임시 음수 id를 생성합니다. + return -createStableHash(seed); +}; + const normalizeAttachment = (attachment: RawChatAttachment): ChatAttachment => ({ id: toNumber(attachment.id), isImage: attachment.isImage, @@ -56,13 +78,18 @@ const normalizeAttachment = (attachment: RawChatAttachment): ChatAttachment => ( createdAt: attachment.createdAt, }); -export const normalizeChatMessage = (message: RawChatMessage): ChatMessage => ({ - id: toNumber(message.id), - content: message.content, - senderId: toNumber(message.senderId ?? message.siteUserId), - createdAt: message.createdAt, - attachments: (message.attachments ?? []).map(normalizeAttachment), -}); +export const normalizeChatMessage = (message: RawChatMessage): ChatMessage => { + const parsedId = toNumber(message.id); + const normalizedId = parsedId > 0 ? parsedId : getFallbackMessageId(message); + + return { + id: normalizedId, + content: message.content, + senderId: toNumber(message.senderId ?? message.siteUserId), + createdAt: message.createdAt, + attachments: (message.attachments ?? []).map(normalizeAttachment), + }; +}; export const normalizeChatPartner = (partner: RawChatPartner): ChatPartner => ({ partnerId: toNumber(partner.partnerId ?? partner.siteUserId), diff --git a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts index 88cef9ef..34742996 100644 --- a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts +++ b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts @@ -6,6 +6,18 @@ import { type ChatMessage, ConnectionStatus } from "@/types/chat"; // --- 프로젝트 내부 의존성 --- import useInfinityScroll from "@/utils/useInfinityScroll"; +const getMessageDedupeKey = (message: ChatMessage): string => { + if (message.id > 0) { + return `id:${message.id}`; + } + + const attachmentKey = message.attachments + .map((attachment) => `${attachment.isImage ? "image" : "file"}:${attachment.url}:${attachment.createdAt}`) + .join(","); + + return `fallback:${message.senderId}:${message.createdAt}:${message.content}:${attachmentKey}`; +}; + const useChatListHandler = (chatId: number) => { // --- 1. State 및 Ref 선언 --- const clientRef = useRef(null); @@ -46,11 +58,12 @@ const useChatListHandler = (chatId: number) => { ); // Deduplicate by id, keeping the last occurrence (chronological order) const dedupedMessages: ChatMessage[] = []; - const seenIds = new Set(); + const seenIds = new Set(); for (let i = sortedMessages.length - 1; i >= 0; i--) { const msg = sortedMessages[i]; - if (!seenIds.has(msg.id)) { - seenIds.add(msg.id); + const dedupeKey = getMessageDedupeKey(msg); + if (!seenIds.has(dedupeKey)) { + seenIds.add(dedupeKey); dedupedMessages.unshift(msg); } } diff --git a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx index 8d0c2ea1..5e2f7d6f 100644 --- a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx +++ b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx @@ -139,10 +139,14 @@ const ChatContent = ({ chatId }: ChatContentProps) => { {/* 첫 번째 메시지에 ref 부착하여 위로 스크롤 시 더 오래된 메시지 로드 */} {messages.map((message, index) => { const showDateSeparator = index === 0 || !isSameDay(messages[index - 1].createdAt, message.createdAt); + const messageKey = + message.id > 0 + ? `message-${message.id}` + : `message-${message.senderId}-${message.createdAt}-${message.content}-${index}`; return (
{/* 날짜 구분선 */} @@ -155,7 +159,6 @@ const ChatContent = ({ chatId }: ChatContentProps) => { )} {/* 일반 채팅 메시지 */}