import { useEffect, useRef, memo, type RefObject } from 'react' import { useMeQuery } from '@/apis' import { useChatStore } from '@/features/chat/stores/useChatStore' import { useChatInitStore } from '@/features/chat/stores/useChatInitStore' import type { ChatMessage as ChatMessageType } from '@/features/chat/types' import { renderEmphasis } from '@/features/chat/lib/emphasis' import { Indicator } from '@/features/chat/components/templates/Indicator' import { Summary } from '@/features/chat/components/templates/Summary' import { ExtraInfoForm } from '@/features/chat/components/templates/ExtraInfoForm' import { BidSummary } from '@/features/chat/components/templates/BidSummary' import { RejectRSP } from '@/features/chat/components/templates/RejectRSP' import { RejectCM } from '@/features/chat/components/templates/RejectCM' const AI_LABEL = '아이마켓코리아 (구매 MD)' export function ChatMessage() { const scrollRef = useRef(null) return (
{/* lg:pt-[64px]: 첫 메시지를 우측 협상절차 카드 상단과 맞추는 값 — 그 카드가 없는 lg 미만에선 스텝바 바로 아래 여백으로만 남아 첫 메시지가 위로 안 붙으므로 뺀다. 스크롤 시 여백도 함께 밀려 올라가도록 스크롤 컨테이너 안쪽에 둔다 */}
) } function ChatList({ scrollRef }: { scrollRef: RefObject }) { const isFirstRef = useRef(true) const chats = useChatStore((s) => s.messages) const isLoading = useChatStore((s) => s.isLoading) // 메시지 추가/타이핑 표시 시 항상 맨 아래로 스크롤. // 스크롤 컨테이너를 직접 내린다 — scrollIntoView 는 요약/부가정보 폼처럼 큰 블록이 뒤늦게 // 레이아웃되면 애니메이션 도중 목표 위치가 밀려 중간에 멈춘다. // 진입 첫 렌더는 즉시(auto) — 히스토리를 복원하며 위에서부터 훑어 내려가는 연출을 막는다. useEffect(() => { const scroller = scrollRef.current if (!scroller) return const behavior: ScrollBehavior = isFirstRef.current ? 'auto' : 'smooth' isFirstRef.current = false const id = requestAnimationFrame(() => { scroller.scrollTo({ top: scroller.scrollHeight, behavior }) }) return () => cancelAnimationFrame(id) }, [chats, isLoading]) if (!chats || chats.length === 0) { return (

채팅 내역이 없습니다.

) } return (
{chats.map((message, index) => ( ))} {isLoading && }
) } // agent 응답을 기다리는 동안(협상 중) 대화 흐름에 표시하는 타이핑 인디케이터(AI 말풍선 톤). function TypingBubble() { return (
{AI_LABEL}
) } const MessageItem = memo(function MessageItem({ message, messages, currentIndex, }: { message: ChatMessageType messages: ChatMessageType[] currentIndex: number }) { const isBot = message.sender === 'bot' // 직전이 reject 폼이면 사용자 답변 말풍선은 숨긴다(폼 자체가 답변을 담고 있음) if (!isBot && currentIndex > 0) { const prev = messages[currentIndex - 1] if (prev?.bot_chat_type === 'rejectRSP' || prev?.bot_chat_type === 'rejectCM') { return null } } return isBot ? : }) const BotMessage = memo(function BotMessage({ message }: { message: ChatMessageType }) { // 인디케이터는 이번 방문에서 '협상중'이었던 세션에서만 표시. const showIndicator = useChatInitStore((s) => s.wasInProgress) const hasScript = Boolean(message.script && message.script.trim()) return (
{AI_LABEL} {hasScript && (
{renderEmphasis(message.script || '')}
)}
{showIndicator && message.bot_chat_type === 'indicator' && message.indicator_value != null && ( )} {message.bot_chat_type === 'summaryRSP' && message.summary && ( <> )} {message.bot_chat_type === 'summaryCM' && message.summary && ( )} {message.bot_chat_type === 'rejectRSP' && } {message.bot_chat_type === 'rejectCM' && }
) }) const UserMessage = memo(function UserMessage({ text }: { text: string }) { const { data: user } = useMeQuery() const label = `${user?.supplierName ?? '공급사'} (나)` return (
{label}
{text}
) })