o2o-negosium-original/front/src/features/chat/components/ItemSection.tsx
민헌 147f9dd007 feat(front): 협상 채팅 페이지 (mock UI)
- 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>
2026-06-17 14:38:04 +09:00

129 lines
4.3 KiB
TypeScript

import { useState, useCallback, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { cn } from '@/lib'
import { ItemImage } from '@/features/chat/components/ItemImage'
import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
export function ItemSection() {
return (
<div className="flex flex-col w-full flex-1 overflow-hidden min-h-0">
<ItemImage />
<ItemInfo />
</div>
)
}
interface TooltipState {
text: string
x: number
y: number
above?: boolean
}
function Tooltip({ text, x, y, above }: TooltipState) {
return createPortal(
<div
className="fixed z-[9999] pointer-events-none"
style={{ left: x, top: y, transform: above ? 'translateY(-100%)' : undefined }}
>
<div className="px-3 py-2 mb-1 rounded-lg bg-neutral-20 text-neutral-90 text-[11px] leading-[16px] whitespace-pre-wrap break-keep max-w-[240px] shadow-md border border-neutral-30">
{text}
</div>
</div>,
document.body,
)
}
function ItemInfo() {
const {
item_code,
item_price,
item_vat_yn,
item_model_name,
item_maker_name,
item_min_order_quantity,
item_lead_time,
item_spec,
item_name,
} = useChatInitStore()
const [tooltip, setTooltip] = useState<TooltipState | null>(null)
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const showTooltip = useCallback((e: React.MouseEvent, text: string, above = false) => {
if (hideTimer.current) clearTimeout(hideTimer.current)
if (above) {
const rect = e.currentTarget.getBoundingClientRect()
setTooltip({ text, x: rect.left, y: rect.top - 8, above })
} else {
setTooltip({ text, x: e.clientX, y: e.clientY + 16, above })
}
}, [])
const hideTooltip = useCallback(() => {
hideTimer.current = setTimeout(() => setTooltip(null), 100)
}, [])
useEffect(() => () => {
if (hideTimer.current) clearTimeout(hideTimer.current)
}, [])
const formatPrice = (price: number) => price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
const isNewItem = !item_code && !item_price
const priceText = isNewItem ? '신규' : `${formatPrice(item_price)}`
const formattedPrice = `${priceText}(${item_vat_yn || 'VAT별도'})`
const renderRow = (title: string, data: string, important = false) => {
const textClass = important ? 'title-3 text-neutral-90' : 'body-3 text-neutral-70'
const displayData = data || '-'
return (
<div className="flex items-start self-stretch gap-1 min-w-0">
<div className={cn(textClass, 'w-[100px]')}>{title}</div>
<div
className={cn(textClass, 'w-[150px] break-keep cursor-default')}
onMouseEnter={(e) => displayData !== '-' && showTooltip(e, displayData)}
onMouseLeave={hideTooltip}
>
{displayData}
</div>
</div>
)
}
return (
<div className="flex flex-col w-full flex-1 overflow-hidden min-h-0">
<div
className="text-center headline-3 text-neutral-90 mb-4 truncate px-8 flex-shrink-0 cursor-default"
onMouseEnter={(e) => item_name && showTooltip(e, item_name)}
onMouseLeave={hideTooltip}
>
{item_name}
</div>
<div className="flex flex-1 items-start self-stretch mr-2 overflow-y-auto pr-6 pl-8 min-h-0">
<div className="flex flex-col items-start self-stretch flex-[1_0_auto] gap-2 min-w-0">
{renderRow('상품코드', item_code, true)}
{renderRow('단가', formattedPrice, true)}
{renderRow('모델명', item_model_name, true)}
{renderRow('제조사', item_maker_name)}
{renderRow('최소주문수량', item_min_order_quantity)}
{renderRow('리드타임', item_lead_time)}
<div className="flex items-start self-stretch gap-1 flex-[1_0]">
<div className="body-3 text-neutral-70 w-[100px]"></div>
<div
className="body-3 text-neutral-70 self-stretch w-0 flex-[1_0] overflow-hidden whitespace-normal break-words break-keep cursor-default"
onMouseEnter={(e) => item_spec && showTooltip(e, item_spec, true)}
onMouseLeave={hideTooltip}
>
{item_spec || '-'}
</div>
</div>
</div>
</div>
{tooltip && <Tooltip {...tooltip} />}
</div>
)
}