o2o-negosium-original/negodata/front/src/features/onboarding/components/DetailSteps.tsx

497 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, type ReactNode } from 'react';
import { AlertTriangle, ArrowRight, Bell, Building2, Check, CheckCircle2, Clock3, FileCheck2, FileText, Handshake, Info, Layers, Link2, Mail, Package, RefreshCw, Target, Trophy, UserCheck, type LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Typography } from '@/components/ui/typography';
import { InfoTip } from './InfoTip';
// 온보딩 상세 단계(1~5). step0(OverviewStep) 뒤에 "하나씩 자세히" 보여주는 정적 패널들.
type HeaderTone = 'user' | 'partner' | 'must' | 'sys';
const NUM_TONE: Record<HeaderTone, string> = {
user: 'bg-primary/10 text-primary',
partner: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
must: 'bg-destructive/10 text-destructive',
sys: 'bg-muted text-muted-foreground',
};
function StepHeader({ num, kicker, actor, title, tone }: { num: number; kicker: string; actor: string; title: string; tone: HeaderTone }) {
const actorCls = tone === 'partner' ? 'text-emerald-600 dark:text-emerald-400' : tone === 'sys' ? 'text-muted-foreground' : 'text-primary';
return (
<header className="mb-4 flex items-center gap-3">
<span className={cn('grid size-9 flex-none place-items-center rounded-[10px] text-base font-extrabold', NUM_TONE[tone])}>{num}</span>
<div>
<Typography variant="label" className="text-[10.5px] text-muted-foreground">
{kicker} · <span className={cn('font-bold', actorCls)}>{actor}</span>
</Typography>
<Typography variant="h4" className="text-lg font-bold tracking-tight">{title}</Typography>
</div>
</header>
);
}
type CalloutTone = 'info' | 'warning' | 'success';
const CALLOUT_TONE: Record<CalloutTone, { wrap: string; icon: string; Icon: typeof Info }> = {
info: { wrap: 'bg-muted/70', icon: 'text-muted-foreground', Icon: Info },
warning: { wrap: 'bg-destructive/10', icon: 'text-destructive', Icon: AlertTriangle },
success: { wrap: 'bg-emerald-500/10', icon: 'text-emerald-600 dark:text-emerald-400', Icon: CheckCircle2 },
};
export function Callout({ tone = 'info', title, children }: { tone?: CalloutTone; title?: string; children: ReactNode }) {
const style = CALLOUT_TONE[tone];
const Icon = style.Icon;
return (
<div className={cn('flex gap-2.5 rounded-lg px-3.5 py-3', style.wrap)}>
<Icon className={cn('mt-0.5 size-4 flex-none', style.icon)} />
<div className="min-w-0">
{title && <Typography variant="small" className={cn('font-extrabold', tone === 'warning' && 'text-destructive')}>{title}</Typography>}
<Typography variant="caption" className={cn('block leading-relaxed text-muted-foreground', title && 'mt-0.5')}>{children}</Typography>
</div>
</div>
);
}
/* ── STEP 1 · 준비 ─────────────────────────────────────────── */
function RegCard({ Icon, sub, name, meta, connect, optional, tone = 'primary' }: { Icon: LucideIcon; sub: string; name: ReactNode; meta: ReactNode; connect?: boolean; optional?: boolean; tone?: 'primary' | 'must' }) {
const activeCls = optional ? 'bg-muted/40 text-muted-foreground' : tone === 'must' ? 'bg-destructive/5' : 'bg-primary/5';
const activeTextCls = tone === 'must' ? 'text-destructive' : 'text-primary';
return (
<div className={cn('relative flex-1 rounded-lg p-4 text-center', connect ? activeCls : 'bg-muted/70')}>
{optional && (
<span className="absolute right-2 top-2 rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold text-muted-foreground">
선택사항
</span>
)}
<Icon className={cn('mx-auto size-6', connect && !optional ? activeTextCls : 'text-muted-foreground')} />
<Typography variant="label" className={cn('mt-2 block text-[11px]', connect && !optional ? activeTextCls : 'text-muted-foreground')}>{sub}</Typography>
<Typography variant="small" className={cn('mt-1.5 block font-bold leading-snug', connect && !optional && activeTextCls)}>{name}</Typography>
<Typography variant="caption" className="mt-1 block text-muted-foreground">{meta}</Typography>
</div>
);
}
const RegArrow = () => <span className="flex items-center px-1.5 text-muted-foreground max-[640px]:justify-center max-[640px]:rotate-90 max-[640px]:py-1"><ArrowRight className="size-4" /></span>;
export function PrepareStep() {
return (
<div>
<StepHeader num={1} kicker="먼저 할 일" actor="내가" tone="user" title="상품·협력사를 등록하고 연결해요" />
<Typography variant="muted" className="mb-5">무엇을 살지(상품), 누구에게 보낼지(협력사), 그리고 그 협력사가 A4용지를 어떤 방식으로 조달하는지 연결해요.</Typography>
<div className="flex items-stretch justify-center gap-1 max-[640px]:flex-col">
<RegCard Icon={Package} sub="무엇을 살까" name="A4용지" meta="기준가 100,000원" />
<RegArrow />
<RegCard Icon={Building2} sub="누구에게 보낼까" name="우리제지" meta={<>담당자 이메일 <Check className="ml-0.5 inline size-3.5 align-text-bottom text-emerald-600 dark:text-emerald-400" /></>} />
<RegArrow />
<RegCard Icon={Link2} sub="어떻게 조달하나" name="우리제지 → A4용지" meta="조달방식: 제조" connect optional />
</div>
<div className="mt-4">
<Callout title="왜 조달방식을 묻나요?">
협력사의 역할이 다르면 참고해야 할 협상 사례도 달라져요. A4용지를 <b className="text-foreground">제조·총판·유통</b> 중 어떤 역할로 취급하는지 저장해두면, 봇이 같은 유형의 결과를 모아 다음 협상카드 선택에 참고합니다.
</Callout>
</div>
</div>
);
}
/* ── STEP 2 · 견적 ─────────────────────────────────────────── */
type QuoteStatusTone = 'auto' | 'manual';
const QUOTE_STATUS_CLS: Record<QuoteStatusTone, string> = {
auto: 'bg-primary/10 text-primary',
manual: 'bg-muted text-muted-foreground',
};
function QuoteStatus({ children, tone }: { children: ReactNode; tone: QuoteStatusTone }) {
return (
<span className={cn('rounded-full px-2 py-0.5 text-[10px] font-extrabold leading-none', QUOTE_STATUS_CLS[tone])}>
{children}
</span>
);
}
function QuoteLine({ Icon, label, status, statusTone = 'manual', value, tip }: { Icon: LucideIcon; label: string; status?: ReactNode; statusTone?: QuoteStatusTone; value: ReactNode; tip: ReactNode }) {
return (
<div className="grid min-h-[64px] grid-cols-[2rem_minmax(0,1fr)_auto] items-center gap-3 px-4 py-3">
<span className="grid size-8 flex-none place-items-center rounded-md bg-muted text-muted-foreground"><Icon className="size-4" /></span>
<span className="min-w-0">
<span className="flex flex-wrap items-center gap-1.5">
<Typography as="span" variant="caption" className="font-bold text-muted-foreground">{label}</Typography>
{status && <QuoteStatus tone={statusTone}>{status}</QuoteStatus>}
</span>
<Typography as="span" variant="small" className="mt-1 block font-extrabold leading-snug text-foreground">{value}</Typography>
</span>
<InfoTip mini align="right">{tip}</InfoTip>
</div>
);
}
export function QuoteStep() {
return (
<div>
<StepHeader num={2} kicker="그다음" actor="내가" tone="user" title="견적을 만들어요" />
<Typography variant="muted" className="mb-5">상품을 고르면 목표가는 자동. 진행 방식을 고르고, 한 곳과 협상할 때만 낙찰 기준을 정해요.</Typography>
<div className="mx-auto max-w-[480px]">
<div className="rounded-xl border border-border bg-card shadow-sm">
<div className="flex items-start justify-between gap-4 rounded-t-xl border-b border-border bg-muted/45 px-5 py-4">
<div className="min-w-0">
<Typography variant="label" className="text-[10px] text-muted-foreground">견적서</Typography>
<Typography variant="h4" className="mt-1 truncate text-lg font-extrabold tracking-tight">A4용지 협상</Typography>
</div>
<div className="rounded-lg bg-background px-3 py-2 text-right">
<Typography as="span" variant="caption" className="block text-[10px] font-bold text-muted-foreground">견적번호</Typography>
<Typography as="span" variant="caption" className="block font-extrabold tabular-nums text-foreground">EST-2026-014</Typography>
</div>
</div>
<div className="divide-y divide-border/80">
<QuoteLine Icon={Handshake} label="진행 방식" status="직접 설정" value="한 곳과 협상 · 1:1" tip={<>여러 후보가 있으면 <b className="text-foreground">여러 곳 경쟁(견적)</b>, 단골 한 곳을 더 깎고 싶으면 <b className="text-foreground">한 곳 협상</b>. 협상카드 밀당은 '한 곳 협상'에서 돌아요.</>} />
<QuoteLine Icon={Package} label="대상" value="A4용지 · 우리제지" tip={<>협상에 부칠 상품과 견적을 보낼 협력사예요. 한 곳 협상은 이 협력사와 1:1로 진행됩니다.</>} />
<QuoteLine Icon={Target} label="가격 기준" status="자동" statusTone="auto" value="목표가 100,000원 · 앵커링가 95,000원" tip={<>목표가는 상품 기준 가격에서 자동으로 잡고, 앵커링가는 <b className="text-foreground">협상 완료 기준선</b>이에요. 협력사 제시가가 앵커링가 이하로 내려오면 협상완료로 넘어갑니다.</>} />
<QuoteLine Icon={Layers} label="협상 한도" status="직접 설정" value="협상카드 선택 · 사용한도 설정" tip={<>한 곳 협상에서 봇이 쓸 협상카드를 고릅니다. 봇은 고른 카드를 한 장씩 쓰며 최대 3번까지 재제안하고, 그때까지 앵커링가 이하로 내려오지 못하면 협상은 실패로 끝납니다.</>} />
<QuoteLine Icon={Trophy} label="낙찰 기준" status="직접 설정" value="앵커링가까지 · 엄격" tip={<>협상이 끝났을 때 <b className="text-foreground">어디까지를 낙찰로 볼지</b> 미리 정해요. 엄격(앵커링가까지)하면 더 싸게 사지만 낙찰이 덜 되고, 느슨(목표가까지)하면 낙찰은 잘 되는 대신 절감폭이 줄어요. (여러 곳 경쟁은 단독 최저가 자동 낙찰.)</>} />
</div>
</div>
<div className="mt-4">
<Callout>
<b className="text-foreground">여러 곳 경쟁 견적은 최저가 낙찰</b>로 진행돼요. <b className="text-foreground">한 곳 협상</b>을 선택했을 때만 앵커링가까지 볼지, 목표가까지 볼지 낙찰 기준을 정합니다.
</Callout>
</div>
</div>
</div>
);
}
export function InviteStep() {
return (
<div>
<StepHeader num={3} kicker="그다음 · 놓치지 마세요" actor="내가" tone="must" title="협력사에게 이메일을 보내요" />
<Typography variant="muted" className="mb-5">여기가 제일 놓치기 쉬운 곳이에요. 견적을 만들었다고 협력사에게 자동으로 알려지는 건 아니에요.</Typography>
<div className="flex items-stretch justify-center gap-1 max-[640px]:flex-col">
<RegCard Icon={FileText} sub="만든 견적" name="A4용지 협상" meta="EST-2026-014" />
<RegArrow />
<RegCard Icon={Mail} sub="꼭 해야 할 일" name="이메일 발송하기" meta="협상 링크 포함" connect tone="must" />
<RegArrow />
<RegCard Icon={Building2} sub="협력사" name="우리제지" meta="링크 받고 입장" />
</div>
<div className="mt-3">
<Callout tone="warning" title="안 보내면 아무 일도 일어나지 않아요">
협력사는 견적이 생긴 줄도 몰라요. 혹시 깜빡해도 <b className="text-foreground">대시보드가 "아직 안 보낸 건"을 콕 집어</b> 알려주니 거기서 바로 보내면 돼요.
</Callout>
</div>
</div>
);
}
/* ── STEP 4 · 협상 ─────────────────────────────────────────── */
type NegotiationZoneTone = 'good' | 'mid' | 'high';
interface NegotiationZone {
id: NegotiationZoneTone;
range: string;
title: string;
action: string;
reply: string;
}
const ANCHOR_PRICE = 95_000;
const TARGET_PRICE = 100_000;
const MIN_OFFER = 93_000;
const MAX_OFFER = 108_000;
const WILD_NEAR_PRICE = Math.floor(ANCHOR_PRICE * 1.02);
const NEGOTIATION_ZONES: NegotiationZone[] = [
{
id: 'good',
range: '95,000원 이하',
title: '바로 확정',
action: '협상 완료 · 투찰 확정',
reply: '좋습니다. 이 금액으로 진행하겠습니다.',
},
{
id: 'mid',
range: '앵커 초과 ~ 앵커×1.02',
title: '막판 와일드카드',
action: '1% 인하 요청',
reply: '거의 맞았습니다. 딱 조금만 더 맞춰주시면 바로 확정할게요.',
},
{
id: 'high',
range: '앵커×1.02 초과',
title: '일반 협상카드',
action: '일반 협상카드로 재제안',
reply: '현재 제시가는 기준보다 높습니다. 조정 가능한 최선의 가격을 알려주세요.',
},
];
const ZONE_STYLE: Record<NegotiationZoneTone, { marker: string; active: string; text: string; track: string }> = {
good: {
marker: 'bg-emerald-600',
active: 'bg-emerald-500/10',
text: 'text-emerald-600 dark:text-emerald-400',
track: '#10b981',
},
mid: {
marker: 'bg-amber-500',
active: 'bg-amber-500/10',
text: 'text-amber-600 dark:text-amber-400',
track: '#f59e0b',
},
high: {
marker: 'bg-primary',
active: 'bg-primary/10',
text: 'text-primary',
track: '#5E6AD2',
},
};
function formatWon(value: number) {
return `${value.toLocaleString('en-US')}원`;
}
function sliderMarkStyle(percent: number) {
const thumbRadius = 10;
return { left: `calc(${percent}% + ${thumbRadius - (percent / 100) * thumbRadius * 2}px)` };
}
function getNegotiationZone(value: number): NegotiationZone {
if (value <= ANCHOR_PRICE) return NEGOTIATION_ZONES[0];
if (value <= WILD_NEAR_PRICE) return NEGOTIATION_ZONES[1];
return NEGOTIATION_ZONES[2];
}
function NegotiationSlider() {
const [offer, setOffer] = useState(102_000);
const zone = getNegotiationZone(offer);
const style = ZONE_STYLE[zone.id];
const pct = ((offer - MIN_OFFER) / (MAX_OFFER - MIN_OFFER)) * 100;
const anchorPct = ((ANCHOR_PRICE - MIN_OFFER) / (MAX_OFFER - MIN_OFFER)) * 100;
const targetPct = ((TARGET_PRICE - MIN_OFFER) / (MAX_OFFER - MIN_OFFER)) * 100;
return (
<div className="rounded-xl bg-muted/70 p-4">
<div className="flex flex-wrap items-end gap-2">
<div>
<Typography variant="caption" className="font-bold text-muted-foreground">협력사 제시가</Typography>
<Typography as="p" variant="h3" className={cn('mt-1 text-2xl font-black tabular-nums', style.text)}>
{formatWon(offer)}
</Typography>
<Typography variant="caption" className="mt-1 block text-muted-foreground">
슬라이더를 움직여 가격 구간별 봇 판단을 확인해보세요.
</Typography>
</div>
<div className="ml-auto rounded-lg bg-background px-3 py-2 text-right">
<Typography variant="caption" className="block font-bold text-muted-foreground">봇 판단</Typography>
<Typography variant="small" className={cn('block font-extrabold', style.text)}>{zone.title}</Typography>
</div>
</div>
<div className="relative mt-5 pb-9">
<input
type="range"
min={MIN_OFFER}
max={MAX_OFFER}
step={500}
value={offer}
onChange={(e) => setOffer(Number(e.target.value))}
className="relative z-10 h-2 w-full cursor-pointer appearance-none rounded-full bg-border accent-primary [&::-moz-range-thumb]:relative [&::-moz-range-thumb]:z-20 [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-4 [&::-moz-range-thumb]:border-background [&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:shadow [&::-webkit-slider-thumb]:relative [&::-webkit-slider-thumb]:z-20 [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-4 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:shadow"
style={{
background: `linear-gradient(to right, ${style.track} 0%, ${style.track} ${pct}%, var(--border) ${pct}%, var(--border) 100%)`,
}}
aria-label="협력사 제시가"
/>
<span className="pointer-events-none absolute top-[15px] h-2 w-px bg-muted-foreground" style={sliderMarkStyle(anchorPct)} />
<span className="pointer-events-none absolute top-[15px] h-2 w-px bg-muted-foreground" style={sliderMarkStyle(targetPct)} />
<Typography as="span" variant="caption" className="absolute top-6 -translate-x-1/2 whitespace-nowrap font-bold text-muted-foreground" style={sliderMarkStyle(anchorPct)}>
앵커링가
</Typography>
<Typography as="span" variant="caption" className="absolute top-6 -translate-x-1/2 whitespace-nowrap font-bold text-muted-foreground" style={sliderMarkStyle(targetPct)}>
목표가
</Typography>
</div>
<div className={cn('mt-6 rounded-lg px-3.5 py-3', style.active)}>
<Typography variant="small" className={cn('font-extrabold', style.text)}>{zone.action}</Typography>
<Typography variant="caption" className="mt-1 block leading-relaxed text-muted-foreground">{zone.reply}</Typography>
</div>
<div className="mt-3 rounded-lg bg-background px-3.5 py-3">
<Typography variant="small" className="font-extrabold text-foreground">그래도 가격을 안 낮추면?</Typography>
<Typography variant="caption" className="mt-1 block leading-relaxed text-muted-foreground">
봇은 <b className="text-foreground">고른 협상카드를 한 장씩 쓰며 최대 3번까지 재제안</b>해요. 그때까지 앵커링가 밑으로 못 내려오면 협상은 실패로 끝나고, 이 경우는 <b className="text-foreground">협상완료·투찰 확정이 아니어서</b> 낙찰 후보가 되지 않습니다.
</Typography>
</div>
</div>
);
}
export function NegotiationStep() {
return (
<div>
<StepHeader num={4} kicker="여기서부턴 자동" actor="봇이" tone="partner" title="봇이 내 대신 협상해요" />
<Typography variant="muted" className="mb-4">협력사가 가격을 부르면, 봇은 제시가가 어느 구간인지 보고 다음 행동을 고릅니다.</Typography>
<NegotiationSlider />
<div className="mt-4">
<Callout>
<b className="text-foreground">협상 완료선은 목표가가 아니라 앵커링가예요.</b> 목표가 아래라도 앵커링가보다 높으면 봇은 한 번 더 낮추도록 유도합니다.
</Callout>
</div>
</div>
);
}
function CloseSignal({ Icon, title, desc }: { Icon: typeof Info; title: string; desc: string }) {
return (
<div className="flex items-start gap-2.5">
<span className="grid size-8 flex-none place-items-center rounded-lg bg-muted text-muted-foreground">
<Icon className="size-4" />
</span>
<div className="min-w-0">
<Typography variant="small" className="font-extrabold">{title}</Typography>
<Typography variant="caption" className="mt-0.5 block leading-relaxed text-muted-foreground">{desc}</Typography>
</div>
</div>
);
}
function CompareModeCard({ title, badge, tone, children }: { title: string; badge: string; tone: 'success' | 'warning'; children: ReactNode }) {
const success = tone === 'success';
return (
<div className={cn('rounded-lg border p-3.5', success ? 'border-emerald-500/30 bg-emerald-500/5' : 'border-primary/25 bg-primary/5')}>
<div className="flex flex-wrap items-center justify-between gap-2">
<Typography variant="small" className="font-black">{title}</Typography>
<span className={cn('rounded-full px-2 py-1 text-[10px] font-extrabold leading-none', success ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' : 'bg-primary/10 text-primary')}>
{badge}
</span>
</div>
<div className="mt-3 space-y-2">{children}</div>
</div>
);
}
function CompareLine({ label, children, tone = 'neutral' }: { label: string; children: ReactNode; tone?: 'success' | 'warning' | 'neutral' }) {
const success = tone === 'success';
const warning = tone === 'warning';
return (
<div className="grid grid-cols-[4.2rem_minmax(0,1fr)] gap-2 rounded-md bg-background/80 px-3 py-2">
<span className={cn('text-[11px] font-extrabold', success ? 'text-emerald-600 dark:text-emerald-400' : warning ? 'text-amber-700 dark:text-amber-400' : 'text-muted-foreground')}>
{label}
</span>
<Typography as="span" variant="caption" className="leading-relaxed text-muted-foreground">{children}</Typography>
</div>
);
}
function FollowupAction({ Icon, title, desc, tone }: { Icon: typeof Info; title: string; desc: ReactNode; tone: 'award' | 'regen' }) {
const regen = tone === 'regen';
return (
<div className={cn('rounded-lg border p-3.5', regen ? 'border-violet-500/30 bg-violet-500/5' : 'border-amber-500/30 bg-amber-500/5')}>
<div className="flex items-start gap-2.5">
<span className={cn('grid size-8 flex-none place-items-center rounded-lg', regen ? 'bg-violet-500/10 text-violet-600 dark:text-violet-400' : 'bg-amber-500/10 text-amber-700 dark:text-amber-400')}>
<Icon className="size-4" />
</span>
<div className="min-w-0">
<Typography variant="small" className={cn('font-black', regen ? 'text-violet-700 dark:text-violet-400' : 'text-amber-700 dark:text-amber-400')}>{title}</Typography>
<Typography variant="caption" className="mt-0.5 block leading-relaxed text-muted-foreground">{desc}</Typography>
</div>
</div>
</div>
);
}
/* ── STEP 5 · 마감/낙찰 ───────────────────────────────────── */
export function AwardStep() {
return (
<div>
<StepHeader num={5} kicker="마지막" actor="자동" tone="sys" title="마감되면 결과를 판정해요" />
<Typography variant="muted" className="mb-5">마감은 견적을 닫고 결과를 확정하는 순간이에요. 경쟁 견적과 1:1 협상은 낙찰을 판단하는 방식이 다릅니다.</Typography>
<div className="rounded-xl border border-emerald-500/30 bg-emerald-500/[0.06] p-4">
<div className="flex items-center gap-3.5 max-[420px]:flex-col max-[420px]:items-stretch max-[420px]:text-center">
<span className="grid size-11 flex-none place-items-center rounded-full bg-emerald-600 text-white max-[420px]:mx-auto">
<Trophy className="size-5" />
</span>
<div className="min-w-0 flex-1">
<Typography variant="label" className="text-[10px] text-emerald-700 dark:text-emerald-400">낙찰 확정 · A4용지 견적</Typography>
<div className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-1 max-[420px]:justify-center">
<Typography as="span" variant="h3" className="text-2xl font-black tracking-tight tabular-nums">94,800원</Typography>
<span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-600/10 px-2 py-0.5">
<Typography as="span" variant="caption" className="font-extrabold text-emerald-700 dark:text-emerald-400">▼ 5,200원 절감</Typography>
<Typography as="span" variant="caption" className="text-muted-foreground">목표가 100,000원 대비</Typography>
</span>
</div>
</div>
<div className="flex-none border-l border-emerald-500/20 pl-3.5 text-right max-[420px]:border-l-0 max-[420px]:border-t max-[420px]:border-emerald-500/20 max-[420px]:pl-0 max-[420px]:pt-3 max-[420px]:text-center">
<Typography variant="caption" className="block text-[10px] text-muted-foreground">낙찰 협력사</Typography>
<Typography variant="small" className="font-bold text-foreground">우리제지</Typography>
</div>
</div>
</div>
<div className="mt-4 rounded-xl border border-border bg-card p-5">
<Typography variant="caption" className="mb-3 block font-extrabold text-muted-foreground">이 결과가 정해지는 방식</Typography>
<div className="grid md:grid-cols-[0.8fr_1.2fr]">
<div className="border-b border-border pb-5 md:border-b-0 md:border-r md:pb-0 md:pr-5">
<Typography variant="caption" className="mb-3 block font-extrabold text-muted-foreground">언제 닫히나요</Typography>
<div className="space-y-4">
<CloseSignal Icon={Clock3} title="마감 시각 도달" desc="설정한 시간이 되면 자동으로 닫아요." />
<CloseSignal Icon={UserCheck} title="전원 협상 종료" desc="시간 전이라도 모든 세션이 끝나면 바로 판정해요." />
<CloseSignal Icon={FileCheck2} title="담당자 수동 마감" desc="필요하면 상세 화면에서 직접 닫을 수 있어요." />
</div>
</div>
<div className="pt-5 md:pl-5 md:pt-0">
<Typography variant="caption" className="mb-3 block font-extrabold text-muted-foreground">방식별 마감 결과</Typography>
<div className="grid gap-3">
<CompareModeCard title="경쟁 견적 (1:N)" badge="최저가만 봄" tone="success">
<CompareLine label="낙찰" tone="success">
투찰가가 있는 협력사 중 <b className="text-foreground">단독 최저가</b>가 있으면 자동 낙찰
</CompareLine>
<CompareLine label="개찰" tone="warning">
최저가가 동가이거나, 아무도 투찰하지 않거나, 전부 협상거부
</CompareLine>
</CompareModeCard>
<CompareModeCard title="1:1 협상" badge="낙찰 기준 적용" tone="warning">
<CompareLine label="낙찰" tone="success">
협상완료 투찰가가 <b className="text-foreground">앵커링가까지 / 목표가까지</b> 중 내가 고른 기준을 통과
</CompareLine>
<CompareLine label="개찰" tone="warning">
기준 미달, 미응찰, 협상거부
</CompareLine>
</CompareModeCard>
</div>
</div>
</div>
<div className="mt-4">
<Callout>
<b className="text-foreground">최저가 동가·전원 미응찰·전부 협상거부·1:1 협상 기준 미달</b> 케이스는 자동 낙찰하지 않고 개찰로 남아요. 이 중 동가·미응찰·가격 기준 미달처럼 다음 라운드가 필요한 경우에는 다음 견적이 자동 재생성돼요.
</Callout>
</div>
</div>
<div className="mt-4 rounded-xl border border-border bg-card p-4">
<Typography variant="caption" className="mb-3 block font-extrabold text-muted-foreground">개찰 후 처리</Typography>
<div className="grid gap-3 sm:grid-cols-2">
<FollowupAction
Icon={Trophy}
title="직접 낙찰"
tone="award"
desc={<>투찰 완료 후보가 있으면 상세 화면에서 협력사를 고르고 낙찰 확정을 누릅니다.</>}
/>
<FollowupAction
Icon={RefreshCw}
title="다음 견적 재생성"
tone="regen"
desc={<>동가·미응찰·가격 기준 미달처럼 다시 견적이 필요한 경우 다음 라운드가 이어집니다.</>}
/>
</div>
<div className="mt-3">
<Callout>
<b className="text-foreground">결과는 알림함으로 오고</b>, 대시보드와 견적 상세에서 낙찰가·절감액·마감 사유를 다시 확인할 수 있어요.
</Callout>
</div>
</div>
<div className="mt-5 flex items-center justify-center gap-2 text-muted-foreground">
<Bell className="size-4" />
<Typography variant="small" className="font-semibold">상품 등록부터 낙찰 결과 확인까지, 여기까지가 전체 흐름이에요.</Typography>
</div>
</div>
);
}