[feat] negosium/front: 협상방 마감 정비 — 우측 패널 납작 섹션·모바일 협상절차 스텝바·이용가이드 모달 토스화·중복 이용가이드 제거·폰트 정리·마감시간 tabular-nums

This commit is contained in:
Mina Choi 2026-07-22 08:29:58 +09:00
parent e9427543c8
commit e7a51b1b75
10 changed files with 84 additions and 57 deletions

View File

@ -36,7 +36,7 @@ function ChatList() {
if (!chats || chats.length === 0) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-base text-neutral-60"> .</p>
<p className="text-sm text-neutral-60"> .</p>
</div>
)
}

View File

@ -1,11 +1,13 @@
import { useChatStore } from '@/features/chat/stores/useChatStore'
import { ChatMessage } from '@/features/chat/components/ChatMessage'
import { UserButton } from '@/features/chat/components/UserButton'
import { MobileStepBar } from '@/features/chat/components/MobileStepBar'
export function ChatSection() {
const { userButtonConfig } = useChatStore()
return (
<div className="flex flex-1 flex-col w-full h-full min-h-0 bg-surface">
<MobileStepBar />
<ChatMessage />
{/* 하단 액션 덱 */}
<div className="shrink-0 border-t border-border bg-white shadow-[0_-4px_20px_rgba(0,0,0,0.03)]">

View File

@ -0,0 +1,46 @@
import { Check } from 'lucide-react'
import { cn } from '@/lib'
import { useChatStore } from '@/features/chat/stores/useChatStore'
import { STEPS } from '@/features/chat/components/menu/NegoStep'
// 모바일 전용: 협상절차를 채팅 상단에 가로 스텝바로 항상 표시한다. (데스크톱은 우측 패널 사용)
export function MobileStepBar() {
const chats = useChatStore((s) => s.messages)
const currentStep = chats[chats.length - 1]?.display_step || STEPS[0].name
const idx = Math.max(
0,
STEPS.findIndex((s) => s.name === currentStep),
)
return (
<div className="shrink-0 border-b border-border bg-white px-4 py-2.5 lg:hidden">
<div className="flex items-center">
{STEPS.map((step, i) => {
const done = i < idx
const current = i === idx
return (
<div key={step.name} className={cn('flex items-center', i < STEPS.length - 1 && 'flex-1')}>
<div
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded-full text-[10px] font-bold transition-colors',
done && 'bg-success text-white',
current && 'bg-brand-600 text-white ring-4 ring-brand-light',
!done && !current && 'border border-border bg-white text-neutral-50',
)}
>
{done ? <Check className="size-3" strokeWidth={3} /> : i + 1}
</div>
{i < STEPS.length - 1 && (
<div className={cn('mx-1 h-0.5 flex-1 rounded-full', i < idx ? 'bg-success' : 'bg-neutral-20')} />
)}
</div>
)
})}
</div>
<p className="mt-1.5 truncate text-[11px] font-bold text-brand-600">
{idx + 1}. {STEPS[idx].name}
<span className="ml-1 font-normal text-neutral-50">{STEPS[idx].desc}</span>
</p>
</div>
)
}

View File

@ -20,7 +20,7 @@ export function RemainingTime() {
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-bold whitespace-nowrap',
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-bold whitespace-nowrap tabular-nums',
ended ? 'bg-neutral-20 text-neutral-60' : 'bg-brand-light text-brand-600',
)}
>

View File

@ -1,33 +1,14 @@
import { useState } from 'react'
import { interactive, cn } from '@/lib'
import { ServiceGuidePopup } from '@/features/chat/components/popup/ServiceGuidePopup'
// 헬프데스크 — 이용 가이드 버튼 클릭 시 안내 팝업을 연다.
// 헬프데스크 — 연락처. (안내 팝업 진입은 상단 '유의사항 및 이용방법' 섹션으로 일원화)
export function Contact() {
const [isOpen, setIsOpen] = useState(false)
return (
<section className="w-full pt-4">
<div className="border-b border-border pb-2">
<span className="text-[11px] font-bold tracking-wide text-neutral-60"></span>
</div>
<div className="flex flex-col gap-2 pt-3">
<button
type="button"
onClick={() => setIsOpen(true)}
className={cn(
'w-fit rounded-lg border border-border bg-white px-3 py-1.5 text-sm font-semibold text-neutral-70 hover:bg-neutral-10',
interactive,
)}
>
</button>
<div className="flex flex-col gap-0.5 text-sm text-neutral-60">
<span>010-0000-0000</span>
<span>o2odev@o2o.kr</span>
</div>
<div className="flex flex-col gap-1 pt-3 text-sm text-neutral-60">
<span>010-0000-0000</span>
<span>o2odev@o2o.kr</span>
</div>
{isOpen && <ServiceGuidePopup onClose={() => setIsOpen(false)} />}
</section>
)
}

View File

@ -7,7 +7,7 @@ interface Props {
setIsOpen: (isOpen: boolean) => void
}
const STEPS = [
export const STEPS = [
{ name: '서비스안내', desc: '협상 방식과 유의사항을 확인합니다.' },
{ name: '담당자확인', desc: '협상 담당자 본인 여부를 확인합니다.' },
{ name: '협상품목안내', desc: '대상 품목과 기준 단가를 확인합니다.' },
@ -61,7 +61,7 @@ export function NegoStep({ isOpen, setIsOpen }: Props) {
<div className="min-w-0">
<p
className={cn(
'text-sm leading-tight',
'text-xs leading-tight',
current && 'font-extrabold text-brand-600',
done && 'font-semibold text-success',
!done && !current && 'font-medium text-neutral-80',
@ -69,7 +69,7 @@ export function NegoStep({ isOpen, setIsOpen }: Props) {
>
{step.name}
</p>
<p className={cn('mt-0.5 text-[11px] leading-snug', current ? 'text-brand-600/80' : 'text-neutral-50')}>
<p className={cn('mt-0.5 text-[10px] leading-snug', current ? 'text-brand-600/80' : 'text-neutral-50')}>
{step.desc}
</p>
</div>

View File

@ -3,14 +3,13 @@ import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
// 협상 유의사항 및 서비스 이용 방법 안내 — 팝업 2종(자동 안내/메뉴 가이드)이 공유하는 본문.
// VAT/배송비 문구는 채팅 init 메타(useChatInitStore)를 읽어 상품별로 동적 표시한다.
// break-keep: 한국어 단어가 중간에서 잘려 줄바꿈되지 않도록 한다.
const textStyle = 'text-[17px] font-normal leading-[150%] tracking-[-0.34px] text-neutral-90 break-keep'
const bodyStyle = 'text-sm font-normal leading-relaxed text-neutral-70 break-keep'
function Bullet({ children }: { children: ReactNode }) {
return (
<div className="flex items-start py-[6px]">
<p className="ml-[8px] mr-[6px] flex-shrink-0"></p>
<div className={`${textStyle} flex-1`}>{children}</div>
<div className="flex items-start gap-2.5 py-1.5">
<span className="mt-[7px] size-1.5 shrink-0 rounded-full bg-brand-600" />
<div className={`${bodyStyle} flex-1`}>{children}</div>
</div>
)
}
@ -23,24 +22,26 @@ export function GuideContent() {
return (
<div className="flex w-full flex-col">
<div className="mb-8 break-keep text-center text-lg font-bold text-neutral-90">
<div className="mb-5 break-keep text-center text-base font-bold text-neutral-90">
</div>
<div className="flex flex-col items-start">
<div className={`${textStyle} mb-2`}>
.
</div>
<div className={`${bodyStyle} mb-2`}>
.
</div>
<div className="flex flex-col divide-y divide-border/60 rounded-xl border border-border px-4 py-1">
<Bullet>
&nbsp;
<span className="font-bold">Negosium </span>
<span className="font-semibold text-neutral-90">
Negosium
</span>
.
</Bullet>
<Bullet>
&nbsp;
<span className="font-bold">
<span className="font-semibold text-neutral-90">
,
</span>
.
@ -48,25 +49,23 @@ export function GuideContent() {
<Bullet>
&nbsp;
<span className="font-bold text-negative">
<span className="font-semibold text-negative">
{vat} {deliveryFee}
</span>
&nbsp;,&nbsp;
<span className="font-bold">
<span className="font-semibold text-neutral-90">
</span>
.
</Bullet>
<Bullet>
,
3 ,
.
,
3 , .
</Bullet>
<Bullet>
,
.
, .
</Bullet>
<Bullet> .</Bullet>
@ -78,13 +77,12 @@ export function GuideContent() {
// 하단 문의 안내 박스
export function GuideContactBox() {
return (
<div className="flex w-full flex-col items-center gap-2 rounded-[16px] bg-neutral-10 p-6">
<div className={`${textStyle} text-center`}>
.
<div className="flex w-full flex-col items-center gap-1.5 rounded-xl bg-neutral-10 p-4">
<div className={`${bodyStyle} text-center`}>
.
</div>
{/* textStyle 의 font-normal 과 같은 요소에서 충돌하지 않도록 bold 는 별도 조합으로 준다 */}
<div className="break-keep text-center text-[17px] font-bold leading-[150%] tracking-[-0.34px] text-neutral-90">
010-0000-0000, 이메일: o2odev@o2o.kr
<div className="break-keep text-center text-sm font-bold text-neutral-90">
010-0000-0000 · o2odev@o2o.kr
</div>
</div>
)

View File

@ -53,7 +53,7 @@ export function RejectCM() {
return (
<div className="flex flex-col w-full p-8 bg-white rounded-2xl border border-border shadow-sm gap-2 transition-all duration-300">
<div className="flex w-full items-center justify-center gap-2 rounded-xl bg-[#FFF1F1] px-4 py-3 text-base font-bold text-[#E5484D]">
<div className="flex w-full items-center justify-center gap-2 rounded-xl bg-[#FFF1F1] px-4 py-3 text-sm font-bold text-[#E5484D]">
<AlertTriangle className="size-5 shrink-0" />
</div>

View File

@ -82,7 +82,7 @@ export function RejectRSP() {
return (
<div className="flex flex-col w-full p-8 bg-white rounded-2xl border border-border shadow-sm gap-2 transition-all duration-300">
<div className="flex w-full items-center justify-center gap-2 rounded-xl bg-[#FFF1F1] px-4 py-3 text-base font-bold text-[#E5484D]">
<div className="flex w-full items-center justify-center gap-2 rounded-xl bg-[#FFF1F1] px-4 py-3 text-sm font-bold text-[#E5484D]">
<AlertTriangle className="size-5 shrink-0" />
</div>

View File

@ -32,13 +32,13 @@ function InputWithUnit({
value={value}
onChange={onChange}
onKeyDown={(e) => e.key === 'Enter' && onSubmit()}
className="h-[52px] flex-1 bg-neutral-10 rounded-xl outline-none border-2 border-brand-600 text-lg font-bold text-neutral-90 placeholder:text-sm placeholder:font-normal placeholder:text-neutral-50 pl-4 pr-[124px]"
className="h-[52px] flex-1 bg-neutral-10 rounded-xl outline-none border-2 border-brand-600 text-base font-bold text-neutral-90 placeholder:text-sm placeholder:font-normal placeholder:text-neutral-50 pl-4 pr-[124px]"
placeholder={placeholder}
aria-label={ariaLabel}
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
/>
<div className="absolute left-4 text-lg font-bold text-neutral-70 pointer-events-none">
<div className="absolute left-4 text-base font-bold text-neutral-70 pointer-events-none">
<span className="opacity-0">{value}</span>
{value && <span className="ml-1">{unit}</span>}
</div>