[feat] negosium: 협상 챗 헤더 정리 — 상품명 대표 + 견적번호

- 헤더 대표값을 협상 중인 상품명으로(여러 협상건 중 '이 건' 식별), 펄스 점·'가격 협상 채널' 문구 제거
- 견적번호(qt_number)를 백엔드 chat init → 프론트로 배선해 헤더 칩으로 노출(좁은 모바일은 숨김)
- 모바일 상품 드로어 상단에 구매사 로고+견적번호(BuyerBrandStrip) — 데스크톱 사이드바 로고의 모바일 대체
This commit is contained in:
Mina Choi 2026-07-29 17:29:15 +09:00
parent 9dca78dc72
commit 9716528ce7
6 changed files with 42 additions and 8 deletions

View File

@ -63,6 +63,7 @@ class Res_ChatInit(Res_WebPacketProtocol):
session_id: str = Field("", description="협상 세션 uuid")
session_status: int = Field(0, description="세션 상태 코드 (SessionStatus: 1=생성 2=진행중 3=완료 4=미참여 5=거부)")
quotation_id: str = Field("", description="소속 견적 uuid")
qt_number: str = Field("", description="견적번호(EST-...)")
quotation_end_time: str = Field("", description="견적 마감 시각 (ISO 8601, 타이머용)")
quotation_memo: str = Field("", description="견적 메모")
item_id: str = Field("", description="상품 uuid")

View File

@ -238,6 +238,7 @@ class ChatService:
res.session_id = str(sess.session_id)
res.session_status = sess.status
res.quotation_id = str(sess.quotation_id)
res.qt_number = sess.qt_number or ""
res.quotation_end_time = quote.end_time.isoformat(timespec="seconds") if quote.end_time else ""
res.quotation_memo = quote.memo or ""
res.item_id = str(item.item_id)

View File

@ -32,6 +32,7 @@ export interface ChatInitResponse {
session_id: string
session_status: number
quotation_id: string
qt_number?: string
quotation_end_time: string
quotation_memo?: string
item_id: string
@ -90,6 +91,7 @@ export function mapInit(r: ChatInitResponse): ChatInitData {
session_status: r.session_status,
item_id: r.item_id,
quotation_id: r.quotation_id,
qt_number: r.qt_number ?? '',
item_name: r.item_name,
item_code: r.item_code ?? '',
item_image: r.item_image ?? '',

View File

@ -16,6 +16,7 @@ const initialState: ChatInitData = {
session_status: 0,
item_id: '',
quotation_id: '',
qt_number: '',
item_name: '',
item_code: '',
item_image: '',

View File

@ -70,6 +70,7 @@ export type ChatInitData = {
session_status: number // SessionStatus 코드 (진입 시점 기준)
item_id: string
quotation_id: string
qt_number: string
item_name: string
item_code: string
item_image: string

View File

@ -2,18 +2,22 @@ import { type ReactNode } from 'react'
import { useNavigate, useSearchParams, Navigate } from 'react-router'
import { ArrowLeft, Package, ClipboardList } from 'lucide-react'
import { cn, interactive } from '@/lib'
import { Logo } from '@/components'
import { MainLayout, MainHeaderBar } from '@/layouts'
import { ChatContainer, ItemSection, RemainingTime } from '@/features/chat'
import { MobileDrawer } from '@/features/chat/components/MobileDrawer'
import { MenuSection } from '@/features/chat/components/menu/MenuSection'
import { useChatUiStore } from '@/features/chat/stores/useChatUiStore'
import { SidebarFooter } from '@/features/auth'
import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
import { SidebarFooter, usePreLoginBranding } from '@/features/auth'
export function ChatPage() {
const [searchParams] = useSearchParams()
const sessionId = searchParams.get('session_id') ?? ''
const drawer = useChatUiStore((s) => s.drawer)
const close = useChatUiStore((s) => s.close)
const qtNumber = useChatInitStore((s) => s.qt_number)
const itemName = useChatInitStore((s) => s.item_name)
// session_id 없이 진입하면 목록으로 되돌린다.
if (!sessionId) return <Navigate to="/list" replace />
@ -27,15 +31,21 @@ export function ChatPage() {
<MainHeaderBar className="px-4 sm:px-6 min-[1180px]:px-8">
{/* min-w-0: flex 기본 min-width:auto 라 자식이 안 줄어들어 헤더 밖으로 넘치던 것 방지 */}
<div className="flex w-full min-w-0 items-center justify-between gap-2">
<div className="flex min-w-0 shrink items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
<BackToListButton />
<span className="hidden h-4 w-px bg-border lg:inline-block" />
<span className="hidden size-2 shrink-0 rounded-full bg-brand-600 animate-pulse lg:inline-block" />
<span className="hidden truncate text-sm font-bold text-neutral-90 lg:inline">
실시간 구매 협상 채널
<span className="h-4 w-px shrink-0 bg-border" />
{/* 헤더 대표값: 협상 중인 상품명 — 여러 협상건 중 '이 건'을 식별하는 기준 */}
<span className="min-w-0 truncate text-sm font-bold text-neutral-90">
{itemName || '가격 협상'}
</span>
{/* 견적번호: 정확한 참조용 보조값. 좁은 모바일에선 숨기고 상품 드로어에서 확인 */}
{qtNumber && (
<span className="hidden shrink-0 rounded-md bg-neutral-10 px-2 py-0.5 font-mono text-xs font-bold tracking-tight text-neutral-70 sm:inline-block">
{qtNumber}
</span>
)}
</div>
<div className="flex min-w-0 shrink items-center gap-1.5">
<div className="flex shrink-0 items-center gap-1.5">
<PanelToggle icon={<Package className="size-4" />} label="상품" panel="product" />
<PanelToggle icon={<ClipboardList className="size-4" />} label="현황" panel="status" />
<RemainingTime />
@ -49,6 +59,8 @@ export function ChatPage() {
{/* 모바일 드로어 (lg 미만) */}
<MobileDrawer open={drawer === 'product'} onClose={close} side="left" title="상품 정보">
{/* 데스크톱 사이드바 상단(구매사 로고)이 모바일에선 숨으므로, 드로어에서 브랜드+견적번호를 노출 */}
<BuyerBrandStrip />
<ItemSection />
<SidebarFooter />
</MobileDrawer>
@ -72,11 +84,27 @@ function BackToListButton() {
)}
>
<ArrowLeft className="size-4" />
<span className="hidden sm:inline">목록으로 돌아가기</span>
<span className="hidden sm:inline">목록</span>
</button>
)
}
// 모바일 상품 드로어 상단: 구매사 로고 + 서비스명 + 견적번호(데스크톱 사이드바 상단의 모바일 대체)
function BuyerBrandStrip() {
const branding = usePreLoginBranding()
const qtNumber = useChatInitStore((s) => s.qt_number)
return (
<div className="flex shrink-0 items-center gap-2 border-b border-border px-6 py-3">
<Logo logoUrl={branding?.logo_url} serviceName={branding?.service_name} size="sm" />
{qtNumber && (
<span className="ml-auto shrink-0 rounded-md bg-neutral-10 px-2 py-0.5 font-mono text-xs font-bold tracking-tight text-neutral-70">
{qtNumber}
</span>
)}
</div>
)
}
// 헤더 우측: 상품/현황 패널 드로어 토글 (모바일 전용)
function PanelToggle({
icon,