- KT-NEGOWIZ /chat 을 현재 아키텍처(Vite·React Router·feature 구조)로 이식 - 사이드 ItemSection(+이미지 확대), 헤더 잔여시간, 채팅(메시지/템플릿/입력), 메뉴 섹션 - 메시지 템플릿: 협상 성공률·협상요약·투찰요약·재협상/재견적 폼 - zustand 스토어 + mock 데이터(정적 쇼케이스, 제출 시 로컬 append) - Next.js·KT 의존 제거(next/image·navigation, 로고·서비스명·연락처, 팝업 보류) - ChatPage 배선: ChatContainer / ItemSection / RemainingTime + auth SidebarFooter 재사용 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
import { useMemo } from 'react'
|
|
import { ChevronDown } from 'lucide-react'
|
|
import { cn } from '@/lib'
|
|
import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
|
|
|
|
interface Props {
|
|
isOpen: boolean
|
|
setIsOpen: (isOpen: boolean) => void
|
|
}
|
|
|
|
export function MDInformation({ isOpen, setIsOpen }: Props) {
|
|
const { quotation_memo } = useChatInitStore()
|
|
|
|
const memoArray = useMemo(() => {
|
|
if (!quotation_memo?.trim()) return []
|
|
return quotation_memo
|
|
.split(/\r?\n+/)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
}, [quotation_memo])
|
|
|
|
if (memoArray.length === 0) return null
|
|
|
|
return (
|
|
<div className="flex flex-col w-full min-w-[200px]">
|
|
<div className="bg-white rounded-[28px]">
|
|
<div className={cn('flex items-center w-full h-[4.75rem] px-4 pl-6', isOpen ? 'pt-6 pb-3' : 'py-6')}>
|
|
<div className="flex items-center flex-1 justify-between">
|
|
<div className="menu-title text-neutral-80">MD 안내사항</div>
|
|
<button
|
|
type="button"
|
|
aria-expanded={isOpen}
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex items-center justify-center w-10 h-10 cursor-pointer text-neutral-70"
|
|
>
|
|
<ChevronDown size={20} className={cn('transition-transform duration-300', !isOpen && 'rotate-180')} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={cn(
|
|
'flex flex-col items-start gap-[19px] w-full px-6 overflow-hidden transition-all duration-300 ease-in-out',
|
|
isOpen ? 'max-h-[200px] pb-8 opacity-100' : 'max-h-0 pb-0 opacity-0',
|
|
)}
|
|
>
|
|
<div className={cn('flex flex-col gap-2 items-start self-stretch max-h-[200px] break-keep', isOpen ? 'overflow-y-auto' : 'overflow-hidden')}>
|
|
{memoArray.map((item, i) => (
|
|
<div key={i} className="flex items-start gap-2 body-3 text-neutral-70">
|
|
<span className="flex-shrink-0">•</span>
|
|
<div className="flex-1 whitespace-pre-wrap">{linkText(item)}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function linkText(text: string) {
|
|
const urlRegex = /(https?:\/\/[^\s]+)/g
|
|
const isUrl = (s: string) => /^https?:\/\/[^\s]+$/.test(s)
|
|
const parts = text.split(urlRegex)
|
|
return (
|
|
<div className="body-3 text-neutral-70">
|
|
{parts.map((part, i) =>
|
|
isUrl(part) ? (
|
|
<a
|
|
key={i}
|
|
href={part}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="block text-info underline cursor-pointer hover:brightness-95 transition-all"
|
|
>
|
|
상품 사이트로 이동
|
|
</a>
|
|
) : (
|
|
<span key={i}>{i > 0 && isUrl(parts[i - 1]) ? part.replace(/^(\s)/, '') : part}</span>
|
|
),
|
|
)}
|
|
</div>
|
|
)
|
|
}
|