feat(chat): 반응형 레이아웃(~1024px) + 리드타임 "일" 표시 + 완료세션 인디케이터 숨김
- 반응형: 최소 폭 1350→1024px, max-[1350px]/max-[1180px] 2단계 축소 - 사이드바 350→310→280, 우측 메뉴 360→320→280, 채팅 거터 140/126→80/72→48/40 (헤더 패딩 동기) - 가격·할인율 입력 min-w 480→420→320, 가운데 버튼 패딩·최소폭 축소, 상품목록 버튼 1180px 미만 아이콘만 - 상품 이미지 220→200, 사이드바 정보 행 값 영역 고정 150px → flex-1 - 리드타임: formatLeadTime 헬퍼 신설 — 좌측 상품정보·협상 결과 요약 카드에 "일" 접미 표시 - 인디케이터: init 의 session_status 를 스토어에 적재, 협상중(IN_PROGRESS) 세션에서만 표시 - 사이드바 정보 영역 의미 없는 가로 스크롤 제거 (shrink 금지 컬럼 + overflow-x 자동 계산이 원인) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
630788d9eb
commit
482c8ed97c
@ -85,6 +85,7 @@ export function mapMessage(w: ChatMessageWire): ChatMessage {
|
||||
export function mapInit(r: ChatInitResponse): ChatInitData {
|
||||
return {
|
||||
session_id: r.session_id,
|
||||
session_status: r.session_status,
|
||||
item_id: r.item_id,
|
||||
quotation_id: r.quotation_id,
|
||||
item_name: r.item_name,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useEffect, useRef, memo } from 'react'
|
||||
import { cn } from '@/lib'
|
||||
import { useChatStore } from '@/features/chat/stores/useChatStore'
|
||||
import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
|
||||
import { SessionStatus } from '@/apis/negotiation/negotiation.type'
|
||||
import type { ChatMessage as ChatMessageType } from '@/features/chat/types'
|
||||
import { Indicator } from '@/features/chat/components/templates/Indicator'
|
||||
import { Summary } from '@/features/chat/components/templates/Summary'
|
||||
@ -12,7 +14,7 @@ export function ChatMessage() {
|
||||
return (
|
||||
<div className="flex-1 w-full min-h-0 pr-[16px]">
|
||||
{/* pt-[64px]: 첫 메시지를 우측 협상절차 카드 상단과 맞추되, 스크롤 시 여백도 함께 밀려 올라가도록 스크롤 컨테이너 안쪽에 둔다 */}
|
||||
<div className="chat-scroll h-full overflow-y-auto flex flex-col pt-[64px] pl-[140px] pr-[126px]">
|
||||
<div className="chat-scroll h-full overflow-y-auto flex flex-col pt-[64px] pl-[140px] pr-[126px] max-[1350px]:pl-[80px] max-[1350px]:pr-[72px] max-[1180px]:pl-[48px] max-[1180px]:pr-[40px]">
|
||||
<ChatList />
|
||||
</div>
|
||||
</div>
|
||||
@ -91,6 +93,9 @@ const MessageItem = memo(function MessageItem({
|
||||
})
|
||||
|
||||
const BotMessage = memo(function BotMessage({ message, isFirst }: { message: ChatMessageType; isFirst?: boolean }) {
|
||||
// 인디케이터는 진행 중인 협상에서만 표시 — 완료/거부 등 결과 열람 재진입 시에는 값이 와도 숨긴다
|
||||
const sessionStatus = useChatInitStore((s) => s.session_status)
|
||||
const showIndicator = sessionStatus === SessionStatus.IN_PROGRESS
|
||||
return (
|
||||
<div className="mb-[56px]">
|
||||
<div className={cn('flex flex-col', !isFirst && 'pt-[36px]')}>
|
||||
@ -98,7 +103,7 @@ const BotMessage = memo(function BotMessage({ message, isFirst }: { message: Cha
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full mt-[32px]">
|
||||
{message.bot_chat_type === 'indicator' && message.indicator_value != null && (
|
||||
{showIndicator && message.bot_chat_type === 'indicator' && message.indicator_value != null && (
|
||||
<Indicator number={message.indicator_value} />
|
||||
)}
|
||||
{message.bot_chat_type === 'summaryRSP' && message.summary && <Summary data={message.summary} />}
|
||||
|
||||
@ -27,10 +27,10 @@ export function ItemImage() {
|
||||
|
||||
function Thumb({ src }: { src: string }) {
|
||||
if (src) {
|
||||
return <img src={src} alt="상품 이미지" className="rounded-[16px] w-[220px] h-[220px] object-cover" />
|
||||
return <img src={src} alt="상품 이미지" className="rounded-[16px] w-[220px] h-[220px] max-[1180px]:w-[200px] max-[1180px]:h-[200px] object-cover" />
|
||||
}
|
||||
return (
|
||||
<div className="flex w-[220px] h-[220px] items-center justify-center rounded-[16px] bg-neutral-20 text-neutral-60">
|
||||
<div className="flex w-[220px] h-[220px] max-[1180px]:w-[200px] max-[1180px]:h-[200px] items-center justify-center rounded-[16px] bg-neutral-20 text-neutral-60">
|
||||
<ImageIcon size={48} strokeWidth={1.5} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'
|
||||
import { cn } from '@/lib'
|
||||
import { ItemImage } from '@/features/chat/components/ItemImage'
|
||||
import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
|
||||
import { formatLeadTime } from '@/features/chat/lib/format'
|
||||
|
||||
export function ItemSection() {
|
||||
return (
|
||||
@ -80,7 +81,7 @@ function ItemInfo() {
|
||||
<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')}
|
||||
className={cn(textClass, 'flex-1 min-w-0 break-words break-keep cursor-default')}
|
||||
onMouseEnter={(e) => displayData !== '-' && showTooltip(e, displayData)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
@ -100,14 +101,14 @@ function ItemInfo() {
|
||||
{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">
|
||||
<div className="flex flex-1 items-start self-stretch mr-2 overflow-y-auto overflow-x-hidden pr-6 pl-8 min-h-0">
|
||||
<div className="flex flex-col items-start self-stretch flex-1 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)}
|
||||
{renderRow('리드타임', formatLeadTime(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>
|
||||
|
||||
@ -6,14 +6,15 @@ import { Percent, Price } from '@/features/chat/components/userInputs'
|
||||
import { GO_TO_LIST_TEXT } from '@/features/chat/lib/userButtonConfig'
|
||||
import type { UserButtonConfig } from '@/features/chat/types'
|
||||
|
||||
// max-[1180px]: 채팅 폭이 좁아지면 버튼 패딩·최소폭 축소, 상품목록은 아이콘만 남긴다
|
||||
const style = {
|
||||
goToList:
|
||||
'flex w-[159px] h-[48px] bg-neutral-40 hover:brightness-[0.97] rounded-[999px] items-center justify-center title-2 text-neutral-80 cursor-pointer gap-2 transition-all ease-out hover:scale-[1.01]',
|
||||
'flex w-[159px] max-[1180px]:w-[48px] h-[48px] bg-neutral-40 hover:brightness-[0.97] rounded-[999px] items-center justify-center title-2 text-neutral-80 cursor-pointer gap-2 max-[1180px]:gap-0 transition-all ease-out hover:scale-[1.01]',
|
||||
black:
|
||||
'flex min-w-[120px] px-[32px] h-[48px] bg-primary hover:brightness-[0.97] rounded-[999px] items-center justify-center title-2 text-primary-foreground cursor-pointer whitespace-nowrap transition-all duration-200 ease-out hover:scale-[1.01]',
|
||||
gray: 'flex min-w-[120px] px-[32px] h-[48px] bg-neutral-60 rounded-[999px] items-center justify-center title-2 text-neutral-00 cursor-pointer whitespace-nowrap transition-all ease-out hover:scale-[1.01]',
|
||||
'flex min-w-[120px] px-[32px] max-[1180px]:min-w-[100px] max-[1180px]:px-[20px] h-[48px] bg-primary hover:brightness-[0.97] rounded-[999px] items-center justify-center title-2 text-primary-foreground cursor-pointer whitespace-nowrap transition-all duration-200 ease-out hover:scale-[1.01]',
|
||||
gray: 'flex min-w-[120px] px-[32px] max-[1180px]:min-w-[100px] max-[1180px]:px-[20px] h-[48px] bg-neutral-60 rounded-[999px] items-center justify-center title-2 text-neutral-00 cursor-pointer whitespace-nowrap transition-all ease-out hover:scale-[1.01]',
|
||||
white:
|
||||
'flex min-w-[120px] px-[32px] h-[48px] bg-neutral-00 rounded-[999px] items-center justify-center title-2 text-neutral-80 border border-neutral-80 cursor-pointer whitespace-nowrap transition-all ease-out hover:scale-[1.01]',
|
||||
'flex min-w-[120px] px-[32px] max-[1180px]:min-w-[100px] max-[1180px]:px-[20px] h-[48px] bg-neutral-00 rounded-[999px] items-center justify-center title-2 text-neutral-80 border border-neutral-80 cursor-pointer whitespace-nowrap transition-all ease-out hover:scale-[1.01]',
|
||||
}
|
||||
|
||||
export function UserButton({ type, text, textList, priceErrorMessage }: UserButtonConfig) {
|
||||
@ -59,9 +60,9 @@ function LoadingDots() {
|
||||
function GoToList() {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<button className={style.goToList} onClick={() => navigate('/list')} aria-label="상품 목록으로 이동">
|
||||
<List size={24} />
|
||||
<span>상품 목록</span>
|
||||
<button className={style.goToList} onClick={() => navigate('/list')} aria-label="상품 목록으로 이동" title="상품 목록">
|
||||
<List size={24} className="shrink-0" />
|
||||
<span className="max-[1180px]:hidden">상품 목록</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export function MenuSection() {
|
||||
const [isStepOpen, setIsStepOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-[360px] h-full pt-[64px] pr-[24px]">
|
||||
<div className="flex flex-col w-[360px] max-[1350px]:w-[320px] max-[1180px]:w-[280px] h-full pt-[64px] pr-[24px]">
|
||||
<div className="flex flex-col flex-1 gap-[28px]">
|
||||
<MDInformation isOpen={isMDOpen} setIsOpen={setIsMDOpen} />
|
||||
<NegoStep isOpen={isStepOpen} setIsOpen={setIsStepOpen} />
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { numberToKorean } from '@/features/chat/lib/koreanNumber'
|
||||
import { formatLeadTime } from '@/features/chat/lib/format'
|
||||
import type { ChatSummary } from '@/features/chat/types'
|
||||
|
||||
// 협상 결과 요약 카드
|
||||
@ -26,7 +27,7 @@ export function Summary({ data }: { data: ChatSummary }) {
|
||||
<DetailText title="제품 규격" value={data.item_spec || '-'} />
|
||||
<DetailText title="최소 주문" value={data.item_moq || '-'} />
|
||||
<DetailText title="배송 형태" value={data.item_delivery_type || '-'} />
|
||||
<DetailText title="배송 리드타임" value={data.item_lead_time || '-'} />
|
||||
<DetailText title="배송 리드타임" value={formatLeadTime(data.item_lead_time) || '-'} />
|
||||
<PriceText price={data.final_price} isVAT={data.item_isVAT} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@ -24,7 +24,7 @@ function InputWithUnit({
|
||||
const errorId = useId()
|
||||
return (
|
||||
<div className="relative flex flex-col gap-3">
|
||||
<div className="relative flex items-center min-w-[480px]">
|
||||
<div className="relative flex items-center min-w-[480px] max-[1350px]:min-w-[420px] max-[1180px]:min-w-[320px]">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
|
||||
5
frontend/src/features/chat/lib/format.ts
Normal file
5
frontend/src/features/chat/lib/format.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// 리드타임 표시값: 백엔드는 일수 숫자를 문자열로 내려주므로 "일"을 붙인다. 이미 "일"로 끝나면 그대로 둔다.
|
||||
export function formatLeadTime(value: string): string {
|
||||
if (!value) return ''
|
||||
return value.endsWith('일') ? value : `${value}일`
|
||||
}
|
||||
@ -8,6 +8,7 @@ interface ChatInitStore extends ChatInitData {
|
||||
|
||||
const initialState: ChatInitData = {
|
||||
session_id: '',
|
||||
session_status: 0,
|
||||
item_id: '',
|
||||
quotation_id: '',
|
||||
item_name: '',
|
||||
|
||||
@ -65,6 +65,7 @@ export type UserButtonConfig = {
|
||||
|
||||
export type ChatInitData = {
|
||||
session_id: string
|
||||
session_status: number // SessionStatus 코드 (진입 시점 기준)
|
||||
item_id: string
|
||||
quotation_id: string
|
||||
item_name: string
|
||||
|
||||
@ -2,19 +2,19 @@ import { type ReactNode } from 'react'
|
||||
import { Logo } from '@/components'
|
||||
import { cn } from '@/lib'
|
||||
|
||||
// 좌측 폭: list=반응형 비율 / chat=고정 350px
|
||||
// 좌측 폭: list=반응형 비율 / chat=고정폭 단계 축소
|
||||
const SIDEBAR_WIDTH = {
|
||||
list:
|
||||
'w-[20%] max-w-[320px] ' +
|
||||
'max-[1520px]:w-[23%] max-[1410px]:w-[25%] ' +
|
||||
'max-[1180px]:w-[27%] max-[1024px]:w-[29%] max-[960px]:w-[33%]',
|
||||
chat: 'w-[350px]',
|
||||
chat: 'w-[350px] max-[1350px]:w-[310px] max-[1180px]:w-[280px]',
|
||||
} as const
|
||||
|
||||
const styles = {
|
||||
root: 'flex w-full min-h-screen max-h-screen bg-background',
|
||||
scrollX: 'flex w-full min-h-screen max-h-screen overflow-x-auto overflow-y-hidden',
|
||||
scrollXInner: 'flex w-full min-h-screen max-h-screen min-w-[1350px] bg-background',
|
||||
scrollXInner: 'flex w-full min-h-screen max-h-screen min-w-[1024px] bg-background',
|
||||
sidebar: 'flex flex-col h-screen pt-2 bg-background',
|
||||
logoHeader:
|
||||
'flex w-full h-[56px] pl-[32px] pr-[24px] py-[8px] items-center justify-between shrink-0',
|
||||
|
||||
@ -19,7 +19,7 @@ export function ChatPage() {
|
||||
logoAction={<ListNavButton />}
|
||||
sidebar={<Sidebar />}
|
||||
header={
|
||||
<MainHeaderBar className="px-[140px]">
|
||||
<MainHeaderBar className="px-[140px] max-[1350px]:px-[80px] max-[1180px]:px-[48px]">
|
||||
<RemainingTime />
|
||||
</MainHeaderBar>
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user