diff --git a/frontend/src/features/chat/components/ChatMessage.tsx b/frontend/src/features/chat/components/ChatMessage.tsx index a8d5c8d..281c44d 100644 --- a/frontend/src/features/chat/components/ChatMessage.tsx +++ b/frontend/src/features/chat/components/ChatMessage.tsx @@ -36,7 +36,7 @@ function ChatList() { if (!chats || chats.length === 0) { return (
-

채팅 내역이 없습니다.

+

채팅 내역이 없습니다.

) } diff --git a/frontend/src/features/chat/components/ChatSection.tsx b/frontend/src/features/chat/components/ChatSection.tsx index bd47e77..cbad604 100644 --- a/frontend/src/features/chat/components/ChatSection.tsx +++ b/frontend/src/features/chat/components/ChatSection.tsx @@ -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 (
+ {/* 하단 액션 덱 */}
diff --git a/frontend/src/features/chat/components/MobileStepBar.tsx b/frontend/src/features/chat/components/MobileStepBar.tsx new file mode 100644 index 0000000..cbb37ea --- /dev/null +++ b/frontend/src/features/chat/components/MobileStepBar.tsx @@ -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 ( +
+
+ {STEPS.map((step, i) => { + const done = i < idx + const current = i === idx + return ( +
+
+ {done ? : i + 1} +
+ {i < STEPS.length - 1 && ( +
+ )} +
+ ) + })} +
+

+ {idx + 1}. {STEPS[idx].name} + {STEPS[idx].desc} +

+
+ ) +} diff --git a/frontend/src/features/chat/components/RemainingTime.tsx b/frontend/src/features/chat/components/RemainingTime.tsx index c631117..b507076 100644 --- a/frontend/src/features/chat/components/RemainingTime.tsx +++ b/frontend/src/features/chat/components/RemainingTime.tsx @@ -20,7 +20,7 @@ export function RemainingTime() { return ( diff --git a/frontend/src/features/chat/components/menu/Contact.tsx b/frontend/src/features/chat/components/menu/Contact.tsx index 4a56ba1..7b2f38a 100644 --- a/frontend/src/features/chat/components/menu/Contact.tsx +++ b/frontend/src/features/chat/components/menu/Contact.tsx @@ -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 (
헬프데스크
-
- -
- 010-0000-0000 - o2odev@o2o.kr -
+
+ 010-0000-0000 + o2odev@o2o.kr
- {isOpen && setIsOpen(false)} />}
) } diff --git a/frontend/src/features/chat/components/menu/NegoStep.tsx b/frontend/src/features/chat/components/menu/NegoStep.tsx index c813955..26f626f 100644 --- a/frontend/src/features/chat/components/menu/NegoStep.tsx +++ b/frontend/src/features/chat/components/menu/NegoStep.tsx @@ -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) {

{step.name}

-

+

{step.desc}

diff --git a/frontend/src/features/chat/components/popup/GuideContent.tsx b/frontend/src/features/chat/components/popup/GuideContent.tsx index 79ad893..4982d00 100644 --- a/frontend/src/features/chat/components/popup/GuideContent.tsx +++ b/frontend/src/features/chat/components/popup/GuideContent.tsx @@ -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 ( -
-

•

-
{children}
+
+ +
{children}
) } @@ -23,24 +22,26 @@ export function GuideContent() { return (
-
+
협상 유의 사항 및 서비스 이용 방법 안내
-
-
- 협상과 관련하여 다음 사항을 참고하여 견적에 참여해 주시기 바랍니다. -
+
+ 협상과 관련하여 다음 사항을 참고하여 견적에 참여해 주시기 바랍니다. +
+
협상 개시는  - Negosium 시스템의 협상 참여 버튼을 클릭하는 순간부터 시작 + + Negosium 시스템의 협상 참여 버튼을 클릭하는 순간부터 시작 + 됩니다. 부여된 협상 시간에  - + 응찰하지 않는 경우, 협상 참여의사가 없는 것으로 간주하여 재견적으로 진행 될 수 있습니다. @@ -48,25 +49,23 @@ export function GuideContent() { 협상에 입력되는 모든 가격은  - + {vat} 및 {deliveryFee}  기준이며,  - + 할인을 요청하는 경우 기존 공급가격에 할인율이 적용된 가격으로 환산 되어 제시됩니다. - 본 협상 결과에 대해서는 협상자와 협상대상자 간의 비밀 유지 조건으로 진행되고, 협상에서 - 얻어진 결과나 내용에 대해서는 당사자를 제외하고 제 3자에 공유할 수 없으며, 비밀 유지를 - 전제로 진행됩니다. + 본 협상 결과에 대해서는 협상자와 협상대상자 간의 비밀 유지 조건으로 진행되고, 협상에서 얻어진 결과나 내용에 + 대해서는 당사자를 제외하고 제 3자에 공유할 수 없으며, 비밀 유지를 전제로 진행됩니다. - 협상이 종결되면 특별한 사유 없이 취소 변경이 불가하니, 신중하게 협상에 참여해 주시기 - 바랍니다. + 협상이 종결되면 특별한 사유 없이 취소 변경이 불가하니, 신중하게 협상에 참여해 주시기 바랍니다. 안내된 사항 외 부분은 기존 견적 프로세스와 동일한 부분 유의 바랍니다. @@ -78,13 +77,12 @@ export function GuideContent() { // 하단 문의 안내 박스 export function GuideContactBox() { return ( -
-
- 기타 협상과정에서 궁금한 사항이나 문의하실 사항은 아래의 연락처로 상담을 부탁드립니다. +
+
+ 기타 협상 과정에서 궁금하거나 문의하실 사항은 아래 연락처로 상담 부탁드립니다.
- {/* textStyle 의 font-normal 과 같은 요소에서 충돌하지 않도록 bold 는 별도 조합으로 준다 */} -
- 헬프데스크 010-0000-0000, 이메일: o2odev@o2o.kr +
+ 헬프데스크 010-0000-0000 · o2odev@o2o.kr
) diff --git a/frontend/src/features/chat/components/templates/RejectCM.tsx b/frontend/src/features/chat/components/templates/RejectCM.tsx index f9d9b41..ded0edf 100644 --- a/frontend/src/features/chat/components/templates/RejectCM.tsx +++ b/frontend/src/features/chat/components/templates/RejectCM.tsx @@ -53,7 +53,7 @@ export function RejectCM() { return (
-
+
최종 공급 희망 가격 입력
diff --git a/frontend/src/features/chat/components/templates/RejectRSP.tsx b/frontend/src/features/chat/components/templates/RejectRSP.tsx index b5eadfe..05b6118 100644 --- a/frontend/src/features/chat/components/templates/RejectRSP.tsx +++ b/frontend/src/features/chat/components/templates/RejectRSP.tsx @@ -82,7 +82,7 @@ export function RejectRSP() { return (
-
+
협상 합의 불가 사유 및 최종 제안 단가 입력
diff --git a/frontend/src/features/chat/components/userInputs.tsx b/frontend/src/features/chat/components/userInputs.tsx index 793d6f8..ab20e89 100644 --- a/frontend/src/features/chat/components/userInputs.tsx +++ b/frontend/src/features/chat/components/userInputs.tsx @@ -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} /> -
+
{value} {value && {unit}}
diff --git a/landing/app/app.css b/landing/app/app.css index e83ae98..14df723 100644 --- a/landing/app/app.css +++ b/landing/app/app.css @@ -19,9 +19,9 @@ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; /* 브랜드 */ - --color-primary: #3182F6; - --color-primary-deep: #1B64DA; /* hover */ - --color-primary-soft: #E8F3FF; /* 연한 파랑 배경 — 봇 말풍선, 배지 */ + --color-primary: #476EFF; + --color-primary-deep: #2F52E0; /* hover */ + --color-primary-soft: #ECF0FF; /* 연한 파랑 배경 — 봇 말풍선, 배지 */ /* 텍스트. ink 는 다크 섹션에선 배경색으로도 쓴다 */ --color-ink: #191F28; @@ -38,6 +38,10 @@ --color-line-strong: #E5E8EB; /* 카드 테두리 */ --color-positive: #0B8F57; /* 낙찰·성공 강조 */ + --color-positive-soft: #E8F7EF; /* 연한 초록 배경 — 낙찰 배지 */ + + --color-accent: #8F52FF; /* 보라 강조 — 결과 탭·히어로 장식 */ + --color-accent-soft: #F4EFFF; /* 연한 보라 배경 */ } html { diff --git a/landing/app/components/sections/comparison.tsx b/landing/app/components/sections/comparison.tsx index c239f20..af6ea76 100644 --- a/landing/app/components/sections/comparison.tsx +++ b/landing/app/components/sections/comparison.tsx @@ -7,22 +7,33 @@ import { SectionHeading } from "@/components/ui/section-heading" import { Typography } from "@/components/ui/typography" import { useFadeUp } from "@/lib/motion" -/** 기존 방식 ↔ 도입 후 토글 비교 — 화면 진입 시 자동으로 오가고, 사용자가 누르면 수동 고정. */ +/** 기존 방식 ↔ 도입 후 토글 비교 — 화면 진입 시 자동으로 오가고, 누르면 잠시 수동(8초 후 자동 재개). */ export function Comparison() { const fadeUp = useFadeUp() const toggleFadeUp = useFadeUp(0.1) const [mode, setMode] = useState("before") - // 사용자가 직접 토글을 누르면 자동 전환 중단 + // 사용자가 토글을 누르면 잠시 자동 전환 중단, 8초 무조작 시 재개 const [locked, setLocked] = useState(false) const [inView, setInView] = useState(false) const sectionRef = useRef(null) + const lockTimerRef = useRef | null>(null) + + const selectMode = (key: Mode) => { + setMode(key) + setLocked(true) + if (lockTimerRef.current) clearTimeout(lockTimerRef.current) + lockTimerRef.current = setTimeout(() => setLocked(false), 8000) + } useEffect(() => { const el = sectionRef.current if (!el) return const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), { threshold: 0.35 }) observer.observe(el) - return () => observer.disconnect() + return () => { + observer.disconnect() + if (lockTimerRef.current) clearTimeout(lockTimerRef.current) + } }, []) useEffect(() => { @@ -32,13 +43,13 @@ export function Comparison() { }, [inView, locked]) return ( -
+
@@ -49,10 +60,7 @@ export function Comparison() { {MODES.map((m) => (