Repository: titicacadev/triple-frontend
Pull request #3601: (Chat) [DMTALK-51] 사용자 식별자 제외 API 응답 마이그레이션
Author: login: dia-triple
Labels: name: tds-widget
Created: 2025-03-19T05:23:44Z | Merged: 2025-03-25T06:14:25Z

<!-- 이 PR을 요약한 내용으로 위 제목 폼을 채워 주세요. -->

## PR 설명

<!-- PR의 목적, PR이 구현하는 기획이나 디자인(figma, slack or jira) 등 리뷰어가 참고할 내용을 적어주세요. -->

- id로 사용자를 특정할 수 없도록 하기 위하여 채팅 룸, 메시지의 member 정보에서 id, identifier, code 등을 제거합니다.
- 기존 룸 내부에서 메시지의 sender와 유저(나)를 구분자로 member.id를 사용하던 부분을 `roomMemberId` 필드를 사용합니다.
- nol-chat의 room 조회 reponse가 [변경됨](https://github.com/titicacadev/nol-chat/pull/138#issuecomment-2742979291)에 따라 해당 타입을 마이그레이션 합니다. 
  -  생성된 룸: [ChatRoomDetailInterface](https://github.com/titicacadev/triple-frontend/pull/3601/files#diff-e55122b1b80ad8814a2c708c4685afbf13ca82929e67e210563eacd9478485efR111)
  - direct/userType/userIdentifier로 진입 시 생성되지 않은 룸: [PreDirectRoomInterface](https://github.com/titicacadev/triple-frontend/pull/3601/files#diff-e55122b1b80ad8814a2c708c4685afbf13ca82929e67e210563eacd9478485efR89)
  - invitation으로 진입 시 생성되지 않은 룸: [InvitationRoomInterface](https://github.com/titicacadev/triple-frontend/pull/3601/files#diff-e55122b1b80ad8814a2c708c4685afbf13ca82929e67e210563eacd9478485efR100)

## 변경 내역

<!-- 실제 변경이 발생한 부분을 위주로 서술해주세요. -->
<!-- 필요하다면 코드 레벨의 설명도 곁들일 수 있습니다. -->
<!-- 리뷰어가 변경점에 대해 빠르게 이해를 할 수 있도록 서술해주세요. -->

## 체크리스트

<!-- 프로젝트별로 반드시 확인해야 하는 항목을 나열해주세요. -->
<!-- 각 항목을 읽어 보시고, 해당하는 항목의 주석을 해제해주세요. -->
<!-- 조금이라도 명확하지 않은 부분이 있다면 슬랙 #triple-web-dev 채널로 질문해주세요! -->
<!-- - [x] 주요 동선의 통합 테스트를 진행하셨나요? -->
<!-- - [x] 기획자/디자이너에게 확인을 받았나요? 혹은 확인이 필요없는 이슈인가요? -->

## 스크린샷 & URL

<!-- 이 변경과 관련있는 스크린샷을 첨부해 주세요. -->
<!-- 반드시 필요한 게 아니라면 생략 가능합니다. -->
<!-- 변경 사항을 확인할 수 있는 샘플 URL을 알려주세요. 바로 동작하는 링크일수록 좋습니다. -->



Diff:
diff --git a/packages/tds-widget/src/chat/chat/chat-room-messages/chat-api-service.ts b/packages/tds-widget/src/chat/chat/chat-room-messages/chat-api-service.ts
index e5474d8527..31bbffc7f4 100644
--- a/packages/tds-widget/src/chat/chat/chat-room-messages/chat-api-service.ts
+++ b/packages/tds-widget/src/chat/chat/chat-room-messages/chat-api-service.ts
@@ -2,6 +2,7 @@ import qs from 'qs'
 
 import {
   ChatMessageInterface,
+  ChatRoomMemberInterface,
   ChatUserInterface,
   HasUnreadOfRoomInterface,
   ReactionType,
@@ -30,7 +31,8 @@ interface ReactionPayload {
 }
 
 interface Sender {
-  id: ChatUserInterface['id']
+  id?: ChatUserInterface['id']
+  roomMemberId?: ChatRoomMemberInterface['roomMemberId']
   profile: {
     name: ChatUserInterface['profile']['name']
   }
@@ -49,6 +51,14 @@ export class ChatApiService<T = UserType> {
     this.isTripleChat = isTripleChat
   }
 
+  public getRoomMemberMe({
+    roomId,
+  }: {
+    roomId: string
+  }): Promise<ChatRoomMemberInterface<T>> {
+    return this.fetcher(`/rooms/${roomId}/members/me`)
+  }
+
   public getMessages({
     roomId,
     backward,
diff --git a/packages/tds-widget/src/chat/chat/chat-room-messages/chat-message-context.tsx b/packages/tds-widget/src/chat/chat/chat-room-messages/chat-message-context.tsx
index 0d3588ec5f..cf6af5319a 100644
--- a/packages/tds-widget/src/chat/chat/chat-room-messages/chat-message-context.tsx
+++ b/packages/tds-widget/src/chat/chat/chat-room-messages/chat-message-context.tsx
@@ -9,6 +9,7 @@ import {
 import {
   type MessagesAction,
   MessagesActions,
+  UnsentMessage,
   useMessagesReducer,
 } from '../messages-reducer'
 import {
@@ -36,8 +37,8 @@ export interface ChatMessagesContextValue<T = UserType> {
    * MessagesReducer의 state 및 dispatch
    */
   messages: ChatMessageInterface<T>[]
-  pendingMessages: ChatMessageInterface<T>[]
-  failedMessages: ChatMessageInterface<T>[]
+  pendingMessages: UnsentMessage<ChatMessageInterface<T>>[]
+  failedMessages: UnsentMessage<ChatMessageInterface<T>>[]
   hasPrevMessage: boolean
   dispatch: Dispatch<
     MessagesAction<ChatMessageInterface<T>, ChatMessageInterface<T>['id']>
diff --git a/packages/tds-widget/src/chat/chat/chat-room-messages/messages.tsx b/packages/tds-widget/src/chat/chat/chat-room-messages/messages.tsx
index 10bf9b1c29..464d288d13 100644
--- a/packages/tds-widget/src/chat/chat/chat-room-messages/messages.tsx
+++ b/packages/tds-widget/src/chat/chat/chat-room-messages/messages.tsx
@@ -4,13 +4,15 @@ import DOMPurify from 'dompurify'
 import {
   ChatMessageInterface,
   ChatMessagePayloadType,
-  ChatUserInterface,
+  ChatRoomUser,
   DisplayTargetAll,
   UserInterface,
   UserType,
 } from '../../types'
 import OriginalMessages from '../../messages'
 import { getProfileImageUrl } from '../../utils'
+import { UnsentMessage } from '../messages-reducer'
+import { getUserIdentifier } from '../../utils/user'
 
 export type ChatRoomMessageInterface<T = UserType> = Omit<
   ChatMessageInterface<T>,
@@ -32,10 +34,10 @@ export default function Messages<T = UserType>({
   showReactions = false,
   ...props
 }: {
-  me: ChatUserInterface<T>
+  me: ChatRoomUser<T>
   messages: ChatMessageInterface<T>[]
-  pendingMessages: ChatMessageInterface<T>[]
-  failedMessages: ChatMessageInterface<T>[]
+  pendingMessages: UnsentMessage<ChatMessageInterface<T>>[]
+  failedMessages: UnsentMessage<ChatMessageInterface<T>>[]
   displayTarget: T
   showReactions?: boolean
 } & Omit<
@@ -64,9 +66,25 @@ export default function Messages<T = UserType>({
 }
 
 function convertMessages<T = UserType>(
-  me: ChatUserInterface<T>,
+  me: ChatRoomUser<T>,
   messages: ChatMessageInterface<T>[],
   roomDisplayTarget: T,
+  showReactions?: boolean,
+): OriginalMessagesPropTypes<T>['messages']
+
+function convertMessages<T = UserType>(
+  me: ChatRoomUser<T>,
+  messages: UnsentMessage<ChatMessageInterface<T>>[],
+  roomDisplayTarget: T,
+  showReactions?: boolean,
+): OriginalMessagesPropTypes<T>['pendingMessages']
+
+function convertMessages<T = UserType>(
+  me: ChatRoomUser<T>,
+  messages:
+    | ChatMessageInterface<T>[]
+    | UnsentMessage<ChatMessageInterface<T>>[],
+  roomDisplayTarget: T,
   showReactions = false,
 ):
   | OriginalMessagesPropTypes<T>['messages']
@@ -138,13 +156,13 @@ function getMessageTypeAndValue<T = UserType>(
 }
 
 function convertChatUserToMessageUser<T = UserType>(
-  me: ChatMessageInterface<T>['sender'] | ChatUserInterface<T>,
+  user: ChatMessageInterface<T>['sender'] | ChatRoomUser<T>,
 ) {
   return {
-    id: me.id,
+    id: getUserIdentifier(user),
     profile: {
-      name: me.profile.name,
-      photo: me.profile.thumbnail || getProfileImageUrl(me),
+      name: user.profile.name,
+      photo: user.profile.thumbnail || getProfileImageUrl(user),
     },
   }
 }
diff --git a/packages/tds-widget/src/chat/chat/chat-room-messages/use-chat-room-messages.ts b/packages/tds-widget/src/chat/chat/chat-room-messages/use-chat-room-messages.ts
index bd72ed7d53..eecde57d50 100644
--- a/packages/tds-widget/src/chat/chat/chat-room-messages/use-chat-room-messages.ts
+++ b/packages/tds-widget/src/chat/chat/chat-room-messages/use-chat-room-messages.ts
@@ -4,16 +4,19 @@ import DOMPurify from 'dompurify'
 import {
   ChatMessageInterface,
   ChatMessagePayloadType,
+  ChatRoomDetailInterface,
   ChatRoomInterface,
-  ChatUserInterface,
-  CreatedChatRoomInterface,
+  ChatRoomMemberInterface,
+  ChatRoomUser,
+  isChatRoomMember,
   isCreatedChatRoom,
   ReactionType,
   UpdatedChatData,
   UserType,
 } from '../../types'
 import { useRoom } from '../room-context'
-import { MessagesActions } from '../messages-reducer'
+import { MessagesActions, UnsentMessage } from '../messages-reducer'
+import { getUserIdentifier } from '../../utils'
 
 import { useScroll } from './use-scroll'
 import { DEFAULT_MESSAGE_PROPERTIES } from './constants'
@@ -25,7 +28,7 @@ import { ChatRoomMessageInterface } from './messages'
 
 interface ChatMessagesProps<T = UserType> {
   defaultMessageProperties?: Partial<ChatMessageInterface<T>>
-  createRoom?: () => Promise<CreatedChatRoomInterface | undefined>
+  createRoom?: () => Promise<ChatRoomDetailInterface | undefined>
 }
 
 export function useChatMessages<T = UserType>({
@@ -34,9 +37,9 @@ export function useChatMessages<T = UserType>({
   >,
   createRoom,
 }: ChatMessagesProps<T> = {}) {
-  const { room, me, updateRoom } = useRoom<
+  const { room, me, updateRoom, updateMe } = useRoom<
     ChatRoomInterface,
-    ChatUserInterface<T>
+    ChatRoomUser<T>
   >()
 
   const { setScrollY, getScrollContainerHeight, triggerScrollToBottom } =
@@ -84,17 +87,16 @@ export function useChatMessages<T = UserType>({
     onError,
   }: {
     roomId: string
-    payload: ChatMessageInterface<T>['payload']
-    sender: ChatMessageInterface<T>['sender']
+    payload: UnsentMessage<ChatMessageInterface<T>>['payload']
+    sender?: UnsentMessage<ChatMessageInterface<T>>['sender']
     tempMessageId?: number
     skipPending?: boolean
     skipFailed?: boolean
     onError?: () => void
   }) {
-    const tempMessage: ChatMessageInterface<T> = {
+    const tempMessage: UnsentMessage<ChatMessageInterface<T>> = {
       id: tempMessageId || new Date().getTime(),
       roomId,
-      senderId: sender.id,
       sender,
       payload,
       ...defaultMessageProperties,
@@ -139,27 +141,49 @@ export function useChatMessages<T = UserType>({
   }
 
   async function handleSendWelcomeMessage({
-    roomId,
+    room,
+    me,
     onError,
   }: {
-    roomId: string
+    room: ChatRoomDetailInterface
+    me: ChatRoomUser<T>
     onError?: () => void
   }) {
     const welcomeMessagesInPending = pendingMessages.filter(
-      (message) => message.sender.id !== me.id,
+      (message) =>
+        message.sender &&
+        getUserIdentifier(message.sender) !== getUserIdentifier(me),
     )
 
+    if (!welcomeMessagesInPending.length) {
+      return
+    }
+
     let allSuccess = true
-    for (const { id: tempId, payload, sender } of welcomeMessagesInPending) {
-      const { success } = await handleSendMessageAction({
-        roomId,
-        tempMessageId: tempId,
-        payload,
-        sender,
-        skipPending: true,
-        skipFailed: true,
-        onError,
-      })
+    for (const {
+      id: tempId,
+      payload,
+      sender,
+      roomId,
+    } of welcomeMessagesInPending) {
+      /**
+       * 룸이 생성되기 전 생성된 welcomeMessage의 sender 정보는 임시 정보(roomMemberId)를 가지므로 업데이트된 룸의 정보에서 sender를 찾아서 사용합니다.
+       */
+      const messageSender = roomId
+        ? sender
+        : findSenderFromRoomMembers(room, me, sender)
+
+      const { success } = messageSender
+        ? await handleSendMessageAction({
+            roomId: room.id,
+            tempMessageId: tempId,
+            payload,
+            sender: messageSender,
+            skipPending: true,
+            skipFailed: true,
+            onError,
+          })
+        : { success: false }
 
       if (!success) {
         allSuccess = false
@@ -183,6 +207,19 @@ export function useChatMessages<T = UserType>({
     return room
   }
 
+  async function getChatRoomMemberId({ roomId }: { roomId: string }) {
+    if (isChatRoomMember(me)) {
+      return me
+    }
+
+    try {
+      const memberMe = await api.getRoomMemberMe({ roomId })
+      updateMe(memberMe)
+      return memberMe
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    } catch (error) {}
+  }
+
   async function onSendMessage(
     payload: ChatMessageInterface<T>['payload'],
     { onError }: { onError?: () => void } = {},
@@ -193,12 +230,23 @@ export function useChatMessages<T = UserType>({
     const currentRoom = await getOrCreateRoom()
     const currentRoomId = 'id' in currentRoom ? currentRoom.id : ''
 
-    if (!currentRoomId) {
-      const tempMessage: ChatMessageInterface<T> = {
+    const skipRoomMemberMe = 'identifier' in me && 'id' in me
+
+    const roomMemberMe =
+      currentRoomId && !skipRoomMemberMe
+        ? await getChatRoomMemberId({ roomId: currentRoomId })
+        : me
+
+    if (
+      !(
+        isCreatedChatRoom(currentRoom) &&
+        roomMemberMe &&
+        (skipRoomMemberMe || isChatRoomMember(roomMemberMe))
+      )
+    ) {
+      const tempMessage: UnsentMessage<ChatMessageInterface<T>> = {
         id: new Date().getTime(),
         roomId: '',
-        senderId: me.id,
-        sender: me,
         payload:
           payload.type === ChatMessagePayloadType.TEXT
             ? { ...payload, message: DOMPurify.sanitize(payload.message) }
@@ -211,12 +259,15 @@ export function useChatMessages<T = UserType>({
 
     /** 첫 렌더링 시에만 자동 메세지를 보내도록 합니다. */
     if (isWelcomeMessagePendingRef.current) {
-      await handleSendWelcomeMessage({ roomId: currentRoomId, onError })
+      await handleSendWelcomeMessage({
+        room: currentRoom as ChatRoomDetailInterface,
+        me: roomMemberMe,
+        onError,
+      })
     }
 
     const { success } = await handleSendMessageAction({
-      roomId: currentRoomId,
-      sender: me,
+      roomId: currentRoom.id,
       payload,
     })
 
@@ -259,7 +310,7 @@ export function useChatMessages<T = UserType>({
   }
 
   function onMessageFailed(
-    tempMessage: ChatMessageInterface<T>,
+    tempMessage: UnsentMessage<ChatMessageInterface<T>>,
     { onError }: { onError?: () => void } = {},
   ) {
     onError?.()
@@ -279,30 +330,30 @@ export function useChatMessages<T = UserType>({
   }
 
   function onRetry(
-    message: ChatRoomMessageInterface<T>,
+    { id, payload }: UnsentMessage<ChatRoomMessageInterface<T>>,
     {
       onComplete,
     }: {
       onComplete?: () => void
     } = {},
   ) {
-    removeUnsentMessages(message)
+    removeUnsentMessages({ id })
 
-    if (message.payload.type !== 'rich') {
-      onSendMessage?.(message.payload)
+    if (payload.type !== 'rich') {
+      onSendMessage?.(payload)
       onComplete?.()
     }
   }
 
   function onRetryCancel(
-    message: ChatRoomMessageInterface<T>,
+    { id }: UnsentMessage<ChatRoomMessageInterface<T>>,
     {
       onComplete,
     }: {
       onComplete?: () => void
     } = {},
   ) {
-    removeUnsentMessages(message)
+    removeUnsentMessages({ id })
     onComplete?.()
   }
 
@@ -318,7 +369,7 @@ export function useChatMessages<T = UserType>({
             pendingMessage와 messages 간의 부드러운 UI 전환을 위해
             me의 메세지일 경우 handleSendMessageAction 함수 내에서 dispatch합니다.
           */
-        if (message.sender.id !== me.id) {
+        if (getUserIdentifier(me) !== getUserIdentifier(message.sender)) {
           dispatch({
             action: MessagesActions.NEW,
             messages: [message],
@@ -328,7 +379,7 @@ export function useChatMessages<T = UserType>({
         triggerScrollToBottom()
       }
     },
-    [dispatch, me.id],
+    [dispatch, (me as ChatRoomMemberInterface).roomMemberId],
   )
 
   /**
@@ -434,3 +485,17 @@ export function useChatMessages<T = UserType>({
     onSendMessageEvent,
   }
 }
+
+function findSenderFromRoomMembers<T>(
+  room: ChatRoomDetailInterface,
+  me: ChatRoomUser<T>,
+  sender?: ChatRoomMemberInterface<T>,
+) {
+  if (sender) {
+    return room.members.find(
+      (member) =>
+        getUserIdentifier(member) === getUserIdentifier(sender) ||
+        (room.isDirect && getUserIdentifier(member) !== getUserIdentifier(me)),
+    ) as ChatRoomMemberInterface<T>
+  }
+}
diff --git a/packages/tds-widget/src/chat/chat/chat-room-messages/use-unread-messages.ts b/packages/tds-widget/src/chat/chat/chat-room-messages/use-unread-messages.ts
index e544267c5c..f1cb83b4e2 100644
--- a/packages/tds-widget/src/chat/chat/chat-room-messages/use-unread-messages.ts
+++ b/packages/tds-widget/src/chat/chat/chat-room-messages/use-unread-messages.ts
@@ -1,12 +1,8 @@
 import { useEffect, useState } from 'react'
 
 import { useRoom } from '../room-context'
-import {
-  CreatedChatRoomInterface,
-  isCreatedChatRoom,
-  OtherUnreadInterface,
-  UserType,
-} from '../../types'
+import { isCreatedChatRoom, OtherUnreadInterface, UserType } from '../../types'
+import { shouldUseLegacyMemberId } from '../../utils'
 
 import { useChatApiService } from './chat-message-context'
 import { ChatRoomMessageInterface } from './messages'
@@ -54,7 +50,8 @@ export function useUnreadMessages<T = UserType>() {
     return otherReadInfo.reduce(
       (prev, info) =>
         Number(info.lastSeenMessageId) < Number(id) &&
-        info.memberId !== sender.id
+        (shouldUseLegacyMemberId(room) ? info.memberId : info.roomMemberId) !==
+          sender.id
           ? prev + 1
           : prev,
       0,
@@ -74,7 +71,7 @@ export function useUnreadMessages<T = UserType>() {
   // TODO pusher unread 이벤트 api 리팩토링 완료시 해당 코드를 삭제합니다.
   useEffect(() => {
     handleUnreadEvent()
-  }, [(room as CreatedChatRoomInterface).id, lastMessageId]) // eslint-disable-line react-hooks/exhaustive-deps
+  }, [(room as { id?: string }).id, lastMessageId]) // eslint-disable-line react-hooks/exhaustive-deps
 
   return {
     calculateUnreadCount,
diff --git a/packages/tds-widget/src/chat/chat/messages-reducer.ts b/packages/tds-widget/src/chat/chat/messages-reducer.ts
index 4507e30dee..fdcb390b63 100644
--- a/packages/tds-widget/src/chat/chat/messages-reducer.ts
+++ b/packages/tds-widget/src/chat/chat/messages-reducer.ts
@@ -13,16 +13,23 @@ export enum MessagesActions {
   HAS_NEXT = 'HAS_NEXT', // 다음 메세지 페이지네이션 플래그 설정
 }
 
-export interface MessageBase<Id = string> {
+export type UnsentMessage<
+  Message extends MessageBase<Id, Sender>,
+  Id = string | number,
+  Sender = unknown,
+> = Omit<Message, 'createdAt' | 'sender'> & { sender?: Message['sender'] }
+
+export interface MessageBase<Id = string | number, Sender = unknown> {
   id: Id
   createdAt?: string
   parentMessage?: MessageBase<Id> | null
+  sender?: Sender
 }
 
 export interface MessagesState<Message extends MessageBase<Id>, Id = string> {
   messages: Message[]
-  pendingMessages: Omit<Message, 'createdAt'>[]
-  failedMessages: Omit<Message, 'createdAt'>[]
+  pendingMessages: UnsentMessage<Message, Id>[]
+  failedMessages: UnsentMessage<Message, Id>[]
   hasPrevMessage: boolean
   hasNextMessage: boolean
 }
@@ -58,11 +65,11 @@ export type MessagesAction<Message extends MessageBase<Id>, Id = string> =
     }
   | {
       action: MessagesActions.PENDING
-      message: Omit<Message, 'createdAt'>
+      message: UnsentMessage<Message, Id>
     }
   | {
       action: MessagesActions.FAIL
-      message: Omit<Message, 'createdAt'>
+      message: UnsentMessage<Message, Id>
     }
   | {
       action: MessagesActions.REMOVE
diff --git a/packages/tds-widget/src/chat/chat/room-context.tsx b/packages/tds-widget/src/chat/chat/room-context.tsx
index b55ef2cd10..7257e1454d 100644
--- a/packages/tds-widget/src/chat/chat/room-context.tsx
+++ b/packages/tds-widget/src/chat/chat/room-context.tsx
@@ -1,30 +1,33 @@
 import { createContext, useContext, PropsWithChildren, useState } from 'react'
 import deepmerge from 'deepmerge'
 
-import type { ChatUserInterface, ChatRoomInterface } from '../types'
+import type { ChatRoomInterface, ChatRoomUser } from '../types'
 
-export interface RoomProviderProps<
-  T = ChatRoomInterface,
-  U = ChatUserInterface,
-> {
+export interface RoomProviderProps<T = ChatRoomInterface, U = ChatRoomUser> {
   room: T
   me: U
 }
 
-interface RoomContextValue<T = ChatRoomInterface, U = ChatUserInterface> {
+interface RoomContextValue<T = ChatRoomInterface, U = ChatRoomUser> {
   room: T
   me: U
   updateRoom: (room: Partial<T>, options?: { deepMerge?: boolean }) => void
+  updateMe: (me: U) => void
 }
 
 export const RoomContext = createContext<RoomContextValue | null>(null)
 
-export function RoomProvider<T = ChatRoomInterface, U = ChatUserInterface>({
+export function RoomProvider<T = ChatRoomInterface, U = ChatRoomUser>({
   room: initialRoom,
-  me,
+  me: initialMe,
   children,
 }: PropsWithChildren<RoomProviderProps<T, U>>) {
   const [room, setRoom] = useState<T>(initialRoom)
+  const [me, setMe] = useState<U>(initialMe)
+
+  const updateMe = (me: U) => {
+    setMe((prev) => ({ ...prev, ...me }))
+  }
 
   const updateRoom = (room: Partial<T>, options?: { deepMerge?: boolean }) => {
     setRoom((prevRoom) =>
@@ -36,12 +39,13 @@ export function RoomProvider<T = ChatRoomInterface, U = ChatUserInterface>({
     room,
     me,
     updateRoom,
+    updateMe,
   } as unknown as RoomContextValue
 
   return <RoomContext.Provider value={value}>{children}</RoomContext.Provider>
 }
 
-export function useRoom<T = ChatRoomInterface, U = ChatUserInterface>() {
+export function useRoom<T = ChatRoomInterface, U = ChatRoomUser>() {
   const context = useContext(RoomContext)
 
   if (!context) {
diff --git a/packages/tds-widget/src/chat/types/message.ts b/packages/tds-widget/src/chat/types/message.ts
index 33074c610d..75ca8e47d5 100644
--- a/packages/tds-widget/src/chat/types/message.ts
+++ b/packages/tds-widget/src/chat/types/message.ts
@@ -1,5 +1,12 @@
+import { UnsentMessage } from '../chat'
+
 import { MetaDataInterface } from './image'
-import { ChatUserInterface, UserType } from './user'
+import {
+  ChatRoomMemberInterface,
+  PreDirectRoomMemberInterface,
+  TripleChatRoomMemberInterface,
+  UserType,
+} from './user'
 
 export type DisplayTargetAll = 'all'
 
@@ -79,25 +86,41 @@ type ChatAlternativeMessagePayload =
   | ChatTextMessagePayload
   | ChatRichMessagePayload
 
+/**
+ * triple-chat 서버 응답으로 받는 ChatMessageInterface
+ */
+export interface TripleChatMessageInterface<T = UserType>
+  extends Omit<ChatMessageInterface<T>, 'sender'> {
+  /**
+   * @deprecated
+   */
+  senderId?: string
+  sender: TripleChatRoomMemberInterface<T>
+}
+
+/**
+ * nol-chat 서버 응답으로 받는 ChatMessageInterface
+ */
 export interface ChatMessageInterface<T = UserType> {
   id: number
   roomId: string
-  senderId: string
   payload: ChatMessagePayload
   createdAt?: string
   displayTarget?: T[] | DisplayTargetAll
   alternative?: ChatAlternativeMessagePayload
   blindedAt?: string
   reactions?: { [type in ReactionType]?: { count: number; haveMine: boolean } }
-  sender: ChatUserInterface<T>
+  sender: ChatRoomMemberInterface<T>
 }
 
-export type WelcomeMessageInterface<T = UserType> = Omit<
-  ChatMessageInterface<T>,
-  'roomId'
-> & {
-  roomId?: string
-}
+export type WelcomeMessageInterface<T = UserType> = UnsentMessage<
+  Omit<ChatMessageInterface<T>, 'roomId' | 'sender'> & {
+    roomId?: string
+    sender:
+      | ChatMessageInterface<T>['sender']
+      | PreDirectRoomMemberInterface<UserType>
+  }
+>
 
 export type CustomerBookingStatus =
   | 'BOOKED'
diff --git a/packages/tds-widget/src/chat/types/room.ts b/packages/tds-widget/src/chat/types/room.ts
index c30e912cdb..6bc34fefbd 100644
--- a/packages/tds-widget/src/chat/types/room.ts
+++ b/packages/tds-widget/src/chat/types/room.ts
@@ -1,6 +1,11 @@
 import { ChatChannelInfo, ValueOf } from './base'
 import { ChatMessageInterface } from './message'
-import { ChatUserInterface, UserType } from './user'
+import {
+  ChatRoomMemberInterface,
+  PreDirectRoomMemberInterface,
+  TripleChatUserInterface,
+  UserType,
+} from './user'
 
 export const RoomType = {
   DEFAULT: 'default', // 기존 파트너센터 챗
@@ -81,82 +86,63 @@ export type ChatRoomMetadata<T, U = ChatRoomMetadataMap> = T extends keyof U
  * @deprecated
  * 기존 트리플 파트너챗에서 /direct로 진입하는 생성되지 않은 채팅방
  */
-interface DirectChatRoomInterface<
-  T = RoomType,
-  U = UserType,
-  V = ChatRoomMetadata<T>,
-> extends BaseChatRoomInterface<T, U, V> {
-  members: ChatUserInterface<U>[]
+export interface PreDirectRoomInterface<T = RoomType, U = UserType> {
+  preDirectRoom: true
+  type: T
+  members: PreDirectRoomMemberInterface<U>[]
+  me: PreDirectRoomMemberInterface<U>
+  other?: PreDirectRoomMemberInterface<U>
 }
 
-interface BaseChatRoomInterface<
+/**
+ * 초대 링크로 진입한 생성되지 않은 RoomInterface
+ */
+export interface InvitationRoomInterface<
   T = RoomType,
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  U = UserType,
   V = ChatRoomMetadata<T>,
 > {
   type: T
   metadata?: V
 }
 
-export interface CreatedChatRoomDetailInterface<
+/**
+ * nol-chat 서버 응답으로 받는 RoomInterface
+ */
+export interface ChatRoomDetailInterface<
   T = RoomType,
   U = UserType,
   V = ChatRoomMetadata<T>,
-> extends BaseChatRoomInterface<T, U, V> {
-  room: ChatRoomDetailRoomInterface<T, U>
-  /**
-   * 채팅방 만료 여부
-   */
-  expired: boolean
-  memberCounts: number
-  members: ChatUserInterface<U>[]
-}
-
-interface ChatRoomDetailRoomInterface<T = RoomType, U = UserType> {
+> {
   id: string
   type: T
   name?: string
   lastMessageId: number
-  lastMessage: ChatMessageInterface<U>
   isDirect: boolean
   createdAt: string
   privateChannel: boolean
   channel: ChatChannelInfo
+  /**
+   * 채팅방 만료 여부
+   */
+  expired: boolean
+  memberCounts: number
+  members: ChatRoomMemberInterface<U>[]
+  metadata?: V
 }
 
-export type CreatedChatRoomInterface<
-  T = RoomType,
-  U = UserType,
-  V = ChatRoomMetadata<T>,
-> = Omit<CreatedChatRoomDetailInterface<T, U, V>, 'room'> &
-  CreatedChatRoomDetailInterface<T, U, V>['room']
-
-/**
- * nol-chat 서버 응답으로 받는 RoomInterface
- */
-export type ChatRoomDetailInterface<
-  T = RoomType,
-  U = UserType,
-  V = ChatRoomMetadata<T>,
-> =
-  | BaseChatRoomInterface<T, U, V>
-  | CreatedChatRoomDetailInterface<T, U, V>
-  | DirectChatRoomInterface<T, U, V>
-
 export interface ChatRoomListItemInterface<T = RoomType, U = UserType>
   extends Pick<
-      CreatedChatRoomInterface<T, U>,
-      | 'id'
-      | 'createdAt'
-      | 'isDirect'
-      | 'name'
-      | 'lastMessageId'
-      | 'lastMessage'
-      | 'type'
-      | 'expired'
-    >,
-    Pick<Required<CreatedChatRoomInterface<T, U>>, 'members'> {
+    ChatRoomDetailInterface<T, U>,
+    | 'id'
+    | 'createdAt'
+    | 'isDirect'
+    | 'name'
+    | 'lastMessageId'
+    | 'type'
+    | 'expired'
+    | 'members'
+  > {
+  lastMessage: ChatMessageInterface<U>
   unreadCount?: number
 }
 
@@ -168,22 +154,37 @@ export type ChatRoomInterface<
   U = UserType,
   V = ChatRoomMetadata<T>,
 > =
-  | BaseChatRoomInterface<T, U, V>
-  | CreatedChatRoomInterface<T, U, V>
-  | DirectChatRoomInterface<T, U, V>
+  | ChatRoomDetailInterface<T, U, V>
+  | InvitationRoomInterface<T, V>
+  | PreDirectRoomInterface<T, U>
+
+/**
+ * @deprecated
+ * 기존 트리플 파트너챗에서 /direct로 진입하는 생성되지 않은 채팅방인지 확인합니다.
+ */
+export function isPreDirectRoom<
+  T = RoomType,
+  U = UserType,
+  V = ChatRoomMetadata<T>,
+>(room: ChatRoomInterface<T, U, V>): room is PreDirectRoomInterface<T, U> {
+  return !!(room as { preDirectRoom?: boolean }).preDirectRoom
+}
 
+/**
+ * 생성된 채팅방인지 확인합니다.
+ */
 export function isCreatedChatRoom<
   T = RoomType,
   U = UserType,
   V = ChatRoomMetadata<T>,
->(room: ChatRoomInterface<T, U, V>): room is CreatedChatRoomInterface<T, U, V> {
+>(room: ChatRoomInterface<T, U, V>): room is ChatRoomDetailInterface<T, U, V> {
   return 'id' in room
 }
 
 /**
  * @deprecated
  * 기존 triple-chat에서 사용하는 RoomInterface
- * nol-chat으로 변경 시 ChatRoomInterface를 사용해 주세요.
+ * nol-chat으로 변경 시 ChatRoomDetailInterface를 사용해 주세요.
  */
 export interface RoomInterface<T = RoomType, U = UserType> {
   id: string
@@ -192,7 +193,7 @@ export interface RoomInterface<T = RoomType, U = UserType> {
   lastMessageId: number
   lastMessage: ChatMessageInterface<U>
   unreadCount?: number
-  members: ChatUserInterface<U>[]
+  members: TripleChatUserInterface<U>[]
   isDirect: boolean
   createdAt: string
   metadata?: EventMetaData
diff --git a/packages/tds-widget/src/chat/types/unread.ts b/packages/tds-widget/src/chat/types/unread.ts
index fa11a17905..2918b64d77 100644
--- a/packages/tds-widget/src/chat/types/unread.ts
+++ b/packages/tds-widget/src/chat/types/unread.ts
@@ -11,7 +11,11 @@ export interface HasUnreadOfRoomInterface extends HasUnreadInterface {
 }
 
 export interface OtherUnreadInterface {
+  /**
+   * @deprecated
+   */
   memberId: string
+  roomMemberId: string
   lastSeenMessageId: number
 }
 
diff --git a/packages/tds-widget/src/chat/types/user.ts b/packages/tds-widget/src/chat/types/user.ts
index 0a581fdf61..5663260d2e 100644
--- a/packages/tds-widget/src/chat/types/user.ts
+++ b/packages/tds-widget/src/chat/types/user.ts
@@ -24,20 +24,73 @@ interface ProfileInterface {
 }
 
 /**
- * triple-chat/nol-chat 서버 응답으로 받는 UserInterface
+ * @deprecated
+ * triple-chat 서버 응답으로 받는 UserInterface
+ */
+export interface TripleChatUserInterface<T = UserType>
+  extends ChatUserInterface<T> {
+  /**
+   * @deprecated
+   */
+  identifier: string
+  /**
+   * @deprecated
+   */
+  code: string
+}
+
+/**
+ * @deprecated
+ * triple-chat 서버 응답으로 받는 RoomMemberInterface
+ */
+export interface TripleChatRoomMemberInterface<T = UserType>
+  extends TripleChatUserInterface<T> {
+  roomMemberId: string
+}
+
+export type ChatRoomUser<T = UserType> =
+  | ChatUserInterface<T>
+  | ChatRoomMemberInterface<T>
+
+/**
+ * nol-chat 서버 응답으로 받는 UserInterface
  */
 export interface ChatUserInterface<T = UserType> {
   id: string
   createdAt: string
   type: T
-  identifier: string
-  code: string
   profile: ChatUserProfileInterface
   channel: ChatChannelInfo
 }
 
+/**
+ * nol-chat 서버 응답으로 받는 RoomMemberInterface
+ */
+export interface ChatRoomMemberInterface<T = UserType>
+  extends Omit<ChatUserInterface<T>, 'id' | 'channel' | 'createdAt'> {
+  roomMemberId: string
+}
+
+/**
+ * @deprecated
+ * 기존 트리플 파트너챗에서 /direct로 진입하는 생성되지 않은 채팅방의 RoomMemberInterface
+ */
+export interface PreDirectRoomMemberInterface<T = UserType>
+  extends Pick<ChatUserInterface<T>, 'id' | 'type' | 'profile'> {
+  /**
+   * 룸이 생성되기 전의 임시 아이디이기 때문에 룸이 생성되면 변경됩니다
+   */
+  roomMemberId: string
+}
+
 interface ChatUserProfileInterface {
   name: string
   thumbnail: string
   message: string
 }
+
+export function isChatRoomMember<T = UserType>(
+  member: ChatRoomMemberInterface<T> | ChatUserInterface<T>,
+): member is ChatRoomMemberInterface<T> {
+  return 'roomMemberId' in member
+}
diff --git a/packages/tds-widget/src/chat/utils/image.ts b/packages/tds-widget/src/chat/utils/image.ts
index 8b6c3e3f5b..e196e3a20c 100644
--- a/packages/tds-widget/src/chat/utils/image.ts
+++ b/packages/tds-widget/src/chat/utils/image.ts
@@ -2,18 +2,20 @@ import {
   MetaDataInterface,
   UserType,
   ChatMessageInterface,
-  ChatUserInterface,
+  ChatRoomUser,
 } from '../types'
 
+import { getUserIdentifier } from './user'
+
 export function getProfileImageUrl<T = UserType>(
-  user: ChatMessageInterface<T>['sender'] | ChatUserInterface<T>,
+  user: ChatMessageInterface<T>['sender'] | ChatRoomUser<T>,
 ) {
   if (user.profile.thumbnail) {
     return user.profile.thumbnail
   } else {
     let imageNumber = 0
     try {
-      imageNumber = parseInt(user.id.substr(0, 4), 16) % 5
+      imageNumber = parseInt(getUserIdentifier(user).substr(0, 4), 16) % 5
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
     } catch (e) {
       return `https://assets.triple.guide/images/ico-random-profile-${imageNumber}@3x.png`
diff --git a/packages/tds-widget/src/chat/utils/index.ts b/packages/tds-widget/src/chat/utils/index.ts
index e584ea4d80..0a6752633b 100644
--- a/packages/tds-widget/src/chat/utils/index.ts
+++ b/packages/tds-widget/src/chat/utils/index.ts
@@ -1,2 +1,3 @@
 export * from './image'
 export { default as useATagNavigator } from './a-tag-navigator'
+export * from './user'
diff --git a/packages/tds-widget/src/chat/utils/user.ts b/packages/tds-widget/src/chat/utils/user.ts
new file mode 100644
index 0000000000..05e88de560
--- /dev/null
+++ b/packages/tds-widget/src/chat/utils/user.ts
@@ -0,0 +1,30 @@
+import {
+  ChatMessageInterface,
+  ChatRoomInterface,
+  ChatRoomUser,
+  isChatRoomMember,
+  UserType,
+} from '../types'
+
+// TODO: nol-chat으로 마이그레이션 후 제거
+export function getUserIdentifier<T = UserType>(
+  user: ChatMessageInterface<T>['sender'] | ChatRoomUser<T>,
+) {
+  /**
+   * triple-chat을 사용한다면 id를 우선적으로 사용하고, 그렇지 않다면 roomMemberId를 사용합니다.
+   */
+  if ('identifier' in user && 'id' in user) {
+    return user.id
+  }
+
+  return isChatRoomMember(user) ? user.roomMemberId : user.id
+}
+
+// TODO: nol-chat으로 마이그레이션 후 제거
+export function shouldUseLegacyMemberId(room: ChatRoomInterface) {
+  return (
+    'members' in room &&
+    'identifier' in room.members[0] &&
+    'id' in room.members[0]
+  )
+}


Reviews:
user: login: choisohyun
body: 요게 바뀐 타입이군요 😳 수고하셨습니다 ! 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T04:37:04Z
user: login: dia-triple
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T04:54:08Z
user: login: choisohyun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T05:03:28Z
user: login: dia-triple
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T05:14:28Z

Review comments:
diff_hunk: @@ -168,22 +154,37 @@ export type ChatRoomInterface<
   U = UserType,
   V = ChatRoomMetadata<T>,
 > =
-  | BaseChatRoomInterface<T, U, V>
-  | CreatedChatRoomInterface<T, U, V>
-  | DirectChatRoomInterface<T, U, V>
+  | ChatRoomDetailInterface<T, U, V>
+  | InvitationRoomInterface<T, V>
+  | PreDirectRoomInterface<T, U>
+
+/**
+ * @deprecated
+ * 기존 트리플 파트너챗에서 /direct로 진입하는 생성되지 않은 채팅방인지 확인합니다.
+ */
+export function isPreDirectRoom<
+  T = RoomType,
+  U = UserType,
+  V = ChatRoomMetadata<T>,
+>(room: ChatRoomInterface<T, U, V>): room is PreDirectRoomInterface<T, U> {
+  return !!(room as { preDirectRoom?: boolean }).preDirectRoom
path: packages/tds-widget/src/chat/types/room.ts
user: login: choisohyun
body: preDirectRoom는 기존 트리플 챗 프론트에서 생성할 값일까요? api에서는 못본 값인 것 같아서요 👀 어떤 시점에서 쓰이는지 궁금합니다..! 
created_at: 2025-03-25T04:33:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,22 +154,37 @@ export type ChatRoomInterface<
   U = UserType,
   V = ChatRoomMetadata<T>,
 > =
-  | BaseChatRoomInterface<T, U, V>
-  | CreatedChatRoomInterface<T, U, V>
-  | DirectChatRoomInterface<T, U, V>
+  | ChatRoomDetailInterface<T, U, V>
+  | InvitationRoomInterface<T, V>
+  | PreDirectRoomInterface<T, U>
+
+/**
+ * @deprecated
+ * 기존 트리플 파트너챗에서 /direct로 진입하는 생성되지 않은 채팅방인지 확인합니다.
+ */
+export function isPreDirectRoom<
+  T = RoomType,
+  U = UserType,
+  V = ChatRoomMetadata<T>,
+>(room: ChatRoomInterface<T, U, V>): room is PreDirectRoomInterface<T, U> {
+  return !!(room as { preDirectRoom?: boolean }).preDirectRoom
path: packages/tds-widget/src/chat/types/room.ts
user: login: dia-triple
body: 아 이것이 `/rooms/direct/${receiverType}/${receiverCode}` 응답 중, 기존에 트리플에서 direct룸 요청했는데 만들어진 룸이 없을떄 반환되는 형태여요! 
이 형태의 응답이 나오게된 이유가 사용자 식별자 제외를 위해 `roomMemberId`를 사용하는데, `roomMemberId`라는 것이 룸이 생성되어야 부여될 수 있는 아이디이고, 이 경우를 제외하고는 룸이 생성되지 않은 상태에서 `members`가 응답되는 케이스가 없어 `preDirectRoom`이라는 것이 생겼습니다 🤣
created_at: 2025-03-25T04:54:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,22 +154,37 @@ export type ChatRoomInterface<
   U = UserType,
   V = ChatRoomMetadata<T>,
 > =
-  | BaseChatRoomInterface<T, U, V>
-  | CreatedChatRoomInterface<T, U, V>
-  | DirectChatRoomInterface<T, U, V>
+  | ChatRoomDetailInterface<T, U, V>
+  | InvitationRoomInterface<T, V>
+  | PreDirectRoomInterface<T, U>
+
+/**
+ * @deprecated
+ * 기존 트리플 파트너챗에서 /direct로 진입하는 생성되지 않은 채팅방인지 확인합니다.
+ */
+export function isPreDirectRoom<
+  T = RoomType,
+  U = UserType,
+  V = ChatRoomMetadata<T>,
+>(room: ChatRoomInterface<T, U, V>): room is PreDirectRoomInterface<T, U> {
+  return !!(room as { preDirectRoom?: boolean }).preDirectRoom
path: packages/tds-widget/src/chat/types/room.ts
user: login: choisohyun
body: 아하...?! 새로 생긴 값이군요
created_at: 2025-03-25T05:03:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,22 +154,37 @@ export type ChatRoomInterface<
   U = UserType,
   V = ChatRoomMetadata<T>,
 > =
-  | BaseChatRoomInterface<T, U, V>
-  | CreatedChatRoomInterface<T, U, V>
-  | DirectChatRoomInterface<T, U, V>
+  | ChatRoomDetailInterface<T, U, V>
+  | InvitationRoomInterface<T, V>
+  | PreDirectRoomInterface<T, U>
+
+/**
+ * @deprecated
+ * 기존 트리플 파트너챗에서 /direct로 진입하는 생성되지 않은 채팅방인지 확인합니다.
+ */
+export function isPreDirectRoom<
+  T = RoomType,
+  U = UserType,
+  V = ChatRoomMetadata<T>,
+>(room: ChatRoomInterface<T, U, V>): room is PreDirectRoomInterface<T, U> {
+  return !!(room as { preDirectRoom?: boolean }).preDirectRoom
path: packages/tds-widget/src/chat/types/room.ts
user: login: dia-triple
body: 네네 마자유!
created_at: 2025-03-25T05:14:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-19T05:25:20Z
body: ## [Codecov](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) Report
Attention: Patch coverage is `0%` with `65 lines` in your changes missing coverage. Please review.
> Project coverage is 13.62%. Comparing base [(`248a2b2`)](https://app.codecov.io/gh/titicacadev/triple-frontend/commit/248a2b2b30d6a409e145b18b4d11914c15c7eb0f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) to head [(`cb42c20`)](https://app.codecov.io/gh/titicacadev/triple-frontend/commit/cb42c201cf44a4afe1caec82793db1b3a4431bbc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev).
> Report is 130 commits behind head on epic/dmtalk.

| [Files with missing lines](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) | Patch % | Lines |
|---|---|---|
| [.../chat/chat-room-messages/use-chat-room-messages.ts](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Fchat%2Fchat-room-messages%2Fuse-chat-room-messages.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC9jaGF0L2NoYXQtcm9vbS1tZXNzYWdlcy91c2UtY2hhdC1yb29tLW1lc3NhZ2VzLnRz) | 0.00% | [34 Missing and 6 partials :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [packages/tds-widget/src/chat/utils/user.ts](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Futils%2Fuser.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC91dGlscy91c2VyLnRz) | 0.00% | [9 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [packages/tds-widget/src/chat/chat/room-context.tsx](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Fchat%2Froom-context.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC9jaGF0L3Jvb20tY29udGV4dC50c3g=) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [packages/tds-widget/src/chat/types/room.ts](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Ftypes%2Froom.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC90eXBlcy9yb29tLnRz) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [...c/chat/chat/chat-room-messages/chat-api-service.ts](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Fchat%2Fchat-room-messages%2Fchat-api-service.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC9jaGF0L2NoYXQtcm9vbS1tZXNzYWdlcy9jaGF0LWFwaS1zZXJ2aWNlLnRz) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [...dget/src/chat/chat/chat-room-messages/messages.tsx](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Fchat%2Fchat-room-messages%2Fmessages.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC9jaGF0L2NoYXQtcm9vbS1tZXNzYWdlcy9tZXNzYWdlcy50c3g=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [packages/tds-widget/src/chat/types/user.ts](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Ftypes%2Fuser.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC90eXBlcy91c2VyLnRz) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [packages/tds-widget/src/chat/utils/image.ts](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Futils%2Fimage.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC91dGlscy9pbWFnZS50cw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |
| [...hat/chat/chat-room-messages/use-unread-messages.ts](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&filepath=packages%2Ftds-widget%2Fsrc%2Fchat%2Fchat%2Fchat-room-messages%2Fuse-unread-messages.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev#diff-cGFja2FnZXMvdGRzLXdpZGdldC9zcmMvY2hhdC9jaGF0L2NoYXQtcm9vbS1tZXNzYWdlcy91c2UtdW5yZWFkLW1lc3NhZ2VzLnRz) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev) |

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff               @@
##           epic/dmtalk    #3601      +/-   ##
===============================================
- Coverage        13.67%   13.62%   -0.06%     
===============================================
  Files              760      761       +1     
  Lines            10849    10894      +45     
  Branches          3741     3766      +25     
===============================================
  Hits              1484     1484              
- Misses            8914     8956      +42     
- Partials           451      454       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/titicacadev/triple-frontend/pull/3601?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=titicacadev).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: dia-triple
created_at: 2025-03-19T05:41:05Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T05:54:42Z
body: v14.0.12-pr-3601.138 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-19T06:58:22Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T07:11:39Z
body: v14.0.12-pr-3601.139 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-19T08:16:01Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T08:34:44Z
body: v14.0.12-pr-3601.139 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-19T08:37:53Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T08:49:07Z
body: v14.0.12-pr-3601.139 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-19T22:54:21Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T23:05:53Z
body: v14.0.12-pr-3601.140 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-19T23:53:28Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T00:04:09Z
body: v14.0.12-pr-3601.142 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-20T03:45:54Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T03:59:21Z
body: v14.0.12-pr-3601.143 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-20T08:57:53Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T09:05:44Z
body: v14.0.12-pr-3601.149 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-21T01:52:00Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-21T02:09:21Z
body: v14.0.12-pr-3601.134 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-21T03:30:01Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-21T03:43:34Z
body: v14.0.12-pr-3601.135 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-21T06:43:04Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-21T06:55:43Z
body: v14.0.12-pr-3601.136 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-21T07:19:29Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-21T07:32:00Z
body: v14.0.12-pr-3601.137 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-21T08:18:54Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-21T08:29:40Z
body: v14.0.12-pr-3601.140 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-21T09:44:08Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-21T09:58:22Z
body: v14.0.12-pr-3601.137 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-24T06:28:22Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-24T06:40:14Z
body: v14.0.12-pr-3601.141 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-24T07:06:07Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-24T07:14:52Z
body: v14.0.12-pr-3601.142 has been published!
author_association: NONE
user: login: dia-triple
created_at: 2025-03-25T02:06:19Z
body: release-canary
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-25T02:18:32Z
body: v14.0.12-pr-3601.149 has been published!
author_association: NONE