[feat] negosium/front: 협상방 마감 정비 — 우측 패널 납작 섹션·모바일 협상절차 스텝바·이용가이드 모달 토스화·중복 이용가이드 제거·폰트 정리·마감시간 tabular-nums
This commit is contained in:
parent
e9427543c8
commit
e7a51b1b75
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)]">
|
||||
|
||||
46
frontend/src/features/chat/components/MobileStepBar.tsx
Normal file
46
frontend/src/features/chat/components/MobileStepBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
협상 개시는
|
||||
<span className="font-bold">Negosium 시스템의 협상 참여 버튼을 클릭하는 순간부터 시작</span>
|
||||
<span className="font-semibold text-neutral-90">
|
||||
Negosium 시스템의 협상 참여 버튼을 클릭하는 순간부터 시작
|
||||
</span>
|
||||
됩니다.
|
||||
</Bullet>
|
||||
|
||||
<Bullet>
|
||||
부여된 협상 시간에
|
||||
<span className="font-bold">
|
||||
<span className="font-semibold text-neutral-90">
|
||||
응찰하지 않는 경우, 협상 참여의사가 없는 것으로 간주하여 재견적으로 진행
|
||||
</span>
|
||||
될 수 있습니다.
|
||||
@ -48,25 +49,23 @@ export function GuideContent() {
|
||||
|
||||
<Bullet>
|
||||
협상에 입력되는 모든 가격은
|
||||
<span className="font-bold text-negative">
|
||||
<span className="font-semibold text-negative">
|
||||
{vat} 및 {deliveryFee}
|
||||
</span>
|
||||
기준이며,
|
||||
<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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user