o2o-infinith-demo/src/pages/PricingPage.tsx

452 lines
19 KiB
TypeScript

/**
* PricingPage — INFINITH Product 1.0 가격 안내 페이지.
*
* 구조 (plan 섹션 15-A):
* 1. Hero (타이틀 + 한 줄 태그라인)
* 2. Billing Toggle (월간 / 연간 20% 할인)
* 3. 3 Tier Cards (INSIGHT / INTELLIGENCE⭐ / INTELLIGENCE+)
* 4. Feature Comparison Table ← Step 3에서 구현
* 5. Free Trial 강조 박스
* 6. Launch Promotion 배너
* 7. FAQ ← Step 3에서 구현
* 8. Enterprise Contact CTA
*
* 유입 추적:
* - `?from=header | footer | cta | hero` 파라미터 읽어 console.log
* (analytics 연동은 Supabase `analytics_events` 테이블 도입 후 후속 작업)
*
* 핵심 데이터 소스:
* - src/data/pricingTiers.ts ← (이 파일 내부에 임시 정의, Step 3에서 분리 고려)
*/
import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router';
import { motion } from 'motion/react';
import { ArrowRight } from 'lucide-react';
import {
CheckFilled,
RocketFilled,
BoltFilled,
PrismFilled,
} from '../components/icons/FilledIcons';
import Badge from '../components/Badge';
import FeatureComparisonTable from '../components/pricing/FeatureComparisonTable';
import FAQ from '../components/pricing/FAQ';
import { buildContactMailto } from '../lib/contact';
// ─── Tier 데이터 정의 ──────────────────────────────────────────────
// plan 섹션 2·5 기준. Feature Comparison Table(Step 3)에서도 재사용 예정.
type TierId = 'insight' | 'intelligence' | 'intelligence-plus';
interface Tier {
id: TierId;
name: string;
tagline: string;
monthlyKRW: number; // 원화 (월 단가 · 계약 기준)
annualMonthlyKRW: number; // 원화 (연 계약 시 월 환산)
annualTotalKRW: number; // 원화 (연 계약 총액)
isPopular?: boolean;
/** 모든 Tier는 계약 기반 영업 — 온라인 결제 없음. 상담 문의 mailto로 통일. */
ctaLabel: string;
bullets: string[];
footnote?: string;
}
const tiers: Tier[] = [
{
id: 'insight',
name: 'INSIGHT',
tagline: '매월 1번, 병원의 온라인 좌표를 점검하세요',
monthlyKRW: 90_000,
annualMonthlyKRW: 72_000,
annualTotalKRW: 864_000,
ctaLabel: '상담 문의',
bullets: [
'월 1회 분석 리포트',
'전 채널 분석 (홈페이지 · 강남언니 · YouTube · Instagram · Facebook · 네이버 플레이스 · 블로그)',
'4주 콘텐츠 플랜',
'경쟁사 추적 1개',
'PDF 내보내기',
],
footnote: '신규 개업의 · 1인 의원 추천',
},
{
id: 'intelligence',
name: 'INTELLIGENCE',
tagline: '경쟁사가 지금 무엇을 바꾸는지, 월 2번 확인하세요',
monthlyKRW: 290_000,
annualMonthlyKRW: 232_000,
annualTotalKRW: 2_784_000,
isPopular: true,
ctaLabel: '상담 문의',
bullets: [
'월 4회 분석 리포트',
'8주 콘텐츠 캘린더 + 주간 KPI 기반 조정',
'Vision AI (의료진·슬로건·인증 자동 추출)',
'경쟁사 추적 3개 · 주간 변동 알림',
'KPI 대시보드 (3/12개월 목표)',
'브랜드 가이드 + 콘텐츠 필러 5종',
'스크린샷 증거 기반 심층 리포트',
],
footnote: '중형 성형외과 · 메인 타겟',
},
{
id: 'intelligence-plus',
name: 'INTELLIGENCE+',
tagline: '매일 변하는 시장에 즉시 대응하세요',
monthlyKRW: 990_000,
annualMonthlyKRW: 792_000,
annualTotalKRW: 9_504_000,
ctaLabel: '상담 문의',
bullets: [
'월 10회 분석 리포트',
'12개월 로드맵 + 월간 전략 리뷰',
'최대 3개 분원 통합 대시보드',
'경쟁사 추적 5개 · 일간 변동 모니터링',
'브랜드 가이드 + 콘텐츠 필러 10종',
'커스텀 리포트 템플릿 (병원 CI 반영)',
'신규 기능 베타 우선 접근',
],
footnote: '대형 · 멀티 분원 병원',
},
];
// ─── 가격 포맷터 ──────────────────────────────────────────────────
// 29_0000원 → "29만원" 포맷. 1000원 단위까지는 표기 안 함 (B2B 가격은 만원 단위)
function formatKRW(amount: number): string {
const man = amount / 10_000;
// 소수점 없는 정수 표기 우선. 999,999 아래면 만원, 이상이면 억 단위까지 확장 가능.
if (Number.isInteger(man)) return `${man.toLocaleString('ko-KR')}만원`;
return `${man.toLocaleString('ko-KR', { maximumFractionDigits: 1 })}만원`;
}
// ─── Billing Toggle 컴포넌트 ──────────────────────────────────────
interface BillingToggleProps {
value: 'monthly' | 'annual';
onChange: (v: 'monthly' | 'annual') => void;
}
const BillingToggle: React.FC<BillingToggleProps> = ({ value, onChange }) => {
return (
<div className="inline-flex items-center gap-1 p-1 rounded-full bg-white/70 border border-slate-200 backdrop-blur-sm shadow-sm">
<button
onClick={() => onChange('monthly')}
className={`px-5 py-2 rounded-full text-sm font-semibold transition-all ${
value === 'monthly'
? 'bg-primary-900 text-white shadow'
: 'text-slate-600 hover:text-primary-900'
}`}
>
</button>
<button
onClick={() => onChange('annual')}
className={`px-5 py-2 rounded-full text-sm font-semibold transition-all flex items-center gap-2 ${
value === 'annual'
? 'bg-primary-900 text-white shadow'
: 'text-slate-600 hover:text-primary-900'
}`}
>
<span
className={`text-xs px-2 py-0.5 rounded-full font-bold ${
value === 'annual'
? 'bg-white/20 text-white'
: 'bg-accent/10 text-accent'
}`}
>
20%
</span>
</button>
</div>
);
};
// ─── Tier Card 컴포넌트 ──────────────────────────────────────────
interface TierCardProps {
tier: Tier;
billing: 'monthly' | 'annual';
onSelect: (tier: Tier) => void;
}
const TierCard: React.FC<TierCardProps> = ({ tier, billing, onSelect }) => {
const price = billing === 'monthly' ? tier.monthlyKRW : tier.annualMonthlyKRW;
const isAnnual = billing === 'annual';
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className={`relative flex flex-col rounded-3xl p-8 bg-white border shadow-sm transition-all hover:shadow-xl ${
tier.isPopular
? 'border-accent/40 ring-2 ring-accent/30 shadow-lg scale-[1.02]'
: 'border-slate-200'
}`}
>
{/* Popular 배지 */}
{tier.isPopular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge variant="popular" size="md" />
</div>
)}
{/* 이름 + 태그라인 */}
<div className="mb-6">
<h3 className="text-2xl font-serif font-bold text-primary-900 mb-2">{tier.name}</h3>
<p className="text-sm text-slate-500 leading-relaxed break-keep">{tier.tagline}</p>
</div>
{/* 가격 */}
<div className="mb-6">
<div className="flex items-baseline gap-2">
<span className="text-4xl font-bold text-primary-900 tracking-tight">
{formatKRW(price)}
</span>
<span className="text-slate-500 text-sm">/</span>
</div>
{isAnnual && (
<p className="text-xs text-accent font-semibold mt-2">
{formatKRW(tier.annualTotalKRW)} · 20%
</p>
)}
{!isAnnual && (
<p className="text-xs text-slate-400 mt-2">
{formatKRW(tier.annualMonthlyKRW)}
</p>
)}
</div>
{/* CTA — DS Primary: gradient + rounded-full (pill) */}
<button
onClick={() => onSelect(tier)}
className={`w-full px-8 py-3.5 rounded-full font-medium text-sm text-white transition-shadow duration-150 shadow-md hover:shadow-xl flex items-center justify-center gap-2 group bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:from-[#AF90FF] hover:to-[#AF90FF] active:scale-[0.98] mb-6 ${
tier.isPopular ? 'shadow-lg' : ''
}`}
>
{tier.ctaLabel}
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</button>
{/* Bullet points — DS FilledIcon(CheckFilled) */}
<ul className="space-y-3 flex-grow">
{tier.bullets.map((bullet, i) => (
<li key={i} className="flex items-start gap-2.5 text-sm text-slate-700">
<CheckFilled size={16} className="text-accent shrink-0 mt-0.5" />
<span className="leading-relaxed break-keep">{bullet}</span>
</li>
))}
</ul>
{/* Footnote */}
{tier.footnote && (
<p className="mt-6 pt-4 border-t border-slate-100 text-xs text-slate-400 text-center">
{tier.footnote}
</p>
)}
</motion.div>
);
};
// ─── 먼저 문의하기 강조 카드 ─────────────────────────────────────
// DS 레퍼런스: CTA 카드 배경에 3-stop warm gradient (Section 2.2)
// + 상단 filled 아이콘 squircle + Primary/Secondary pill 버튼 병렬
//
// PART III 피봇: "첫 리포트 무료 / 카드 등록 불필요" 표현 삭제 → 계약 기반 영업으로 일원화.
function ContactFirstBox() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="relative w-full rounded-3xl p-8 md:p-12 text-center overflow-hidden bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] border border-white/40 shadow-[3px_4px_12px_rgba(0,0,0,0.06)]"
>
{/* 상단 아이콘 squircle — DS filled icon 규칙 */}
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-white/60 backdrop-blur-sm border border-white/50 mb-6">
<BoltFilled size={26} className="text-accent" />
</div>
<h3 className="text-2xl md:text-3xl font-serif font-bold text-primary-900 mb-3">
</h3>
<p className="text-slate-600 leading-relaxed max-w-xl mx-auto mb-8 break-keep">
· ,
.
</p>
{/* DS 듀얼 버튼: Primary + Secondary — 모두 rounded-full (pill) */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<a
href={buildContactMailto('도입 상담 문의')}
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full font-medium text-sm text-white transition-shadow duration-150 shadow-md hover:shadow-xl group bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:from-[#AF90FF] hover:to-[#AF90FF] active:scale-[0.98]"
>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</a>
<a
href="#tiers"
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full font-medium text-sm bg-white border border-slate-200 text-[#021341] hover:bg-slate-50 transition-colors"
>
</a>
</div>
</motion.div>
);
}
// ─── Launch Promotion 배너 ───────────────────────────────────────
// DS: 🎁 이모지 금지(Principle: No Emoji) → RocketFilled 대체
// dark primary gradient card + Secondary pill 버튼
function PromotionBanner() {
return (
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="w-full rounded-3xl bg-gradient-to-r from-[#4F1DA1] to-[#021341] text-white p-6 md:p-8 flex flex-col md:flex-row items-center gap-4 md:gap-6"
>
{/* Filled icon squircle — 🎁 대체 */}
<div className="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-white/10 backdrop-blur-sm shrink-0">
<RocketFilled size={24} className="text-white" />
</div>
<div className="flex-1 text-center md:text-left">
<p className="text-xs font-semibold text-purple-200 mb-1 tracking-wide">
· 20
</p>
<p className="text-base md:text-lg font-serif">
INTELLIGENCE · INTELLIGENCE+ <strong>3 30% </strong>
</p>
</div>
<a
href={buildContactMailto('런칭 프로모션 문의')}
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-full bg-white border border-slate-200 text-[#021341] font-medium text-sm hover:bg-slate-50 transition-colors whitespace-nowrap"
>
</a>
</motion.div>
);
}
// ─── Enterprise Contact CTA ──────────────────────────────────────
// DS: outlined 패턴은 DS에 없음 → Primary pill(gradient + rounded-full)
function EnterpriseContact() {
return (
// outer: w-full로 섹션 정렬 / inner 콘텐츠만 max-w-2xl로 가독성 유지
<div className="w-full text-center">
<div className="max-w-2xl mx-auto">
<h3 className="text-2xl font-serif font-bold text-primary-900 mb-3">
, ?
</h3>
<p className="text-slate-600 mb-8 break-keep">
4 , .
</p>
<a
href={buildContactMailto('커스텀 플랜 문의')}
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full font-medium text-sm text-white transition-shadow duration-150 shadow-md hover:shadow-xl group bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:from-[#AF90FF] hover:to-[#AF90FF] active:scale-[0.98]"
>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</a>
</div>
</div>
);
}
// ─── PricingPage 본체 ────────────────────────────────────────────
export default function PricingPage() {
const [searchParams] = useSearchParams();
const [billing, setBilling] = useState<'monthly' | 'annual'>('annual');
// 유입 소스 추적 — 추후 Supabase `analytics_events` 테이블로 전송
useEffect(() => {
const from = searchParams.get('from');
if (from) {
// TODO(analytics): Supabase analytics_events insert
console.info(`[pricing] referred from: ${from}`);
}
}, [searchParams]);
/**
* Tier 선택 핸들러 — 계약 기반 영업.
* 온라인 결제 없이 모든 Tier를 상담 문의 mailto로 통일.
* Subject에 Tier 이름을 넣어 영업팀이 유입 경로를 구분.
*/
const handleTierSelect = (tier: Tier) => {
window.location.href = buildContactMailto(`${tier.name} 플랜 상담 문의`);
};
return (
<main className="relative pt-28 md:pt-32 pb-24 overflow-hidden">
{/* Background — 랜딩 Hero와 톤 통일 */}
<div className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-100 via-purple-50 to-pink-50 opacity-60" />
{/* ── Section 1 · Hero ─────────────────────────── */}
<section className="px-6 text-center max-w-4xl mx-auto mb-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/70 border border-white/40 text-xs font-bold text-accent mb-6">
<PrismFilled size={14} className="text-accent" />
Pricing ·
</div>
<h1 className="text-4xl md:text-6xl font-serif font-bold text-primary-900 leading-[1.1] tracking-[-0.02em] mb-5">
Strategic Planning,<br className="hidden md:block" />
<span className="text-gradient">At Your Scale.</span>
</h1>
<p className="text-lg text-slate-600 max-w-2xl mx-auto mb-10 break-keep">
. ·· .
</p>
</motion.div>
{/* ── Section 2 · Billing Toggle ────────────── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<BillingToggle value={billing} onChange={setBilling} />
</motion.div>
</section>
{/* ── Section 3 · 3 Tier Cards ─────────────────── */}
<section id="tiers" className="px-6 max-w-7xl mx-auto mb-20 scroll-mt-24">
<div className="grid md:grid-cols-3 gap-6 md:gap-8 items-stretch">
{tiers.map((tier) => (
<TierCard key={tier.id} tier={tier} billing={billing} onSelect={handleTierSelect} />
))}
</div>
</section>
{/* ── Section 4 · Feature Comparison Table ─── */}
<section className="px-6 mb-20 max-w-7xl mx-auto">
<FeatureComparisonTable />
</section>
{/* ── Section 5 · 먼저 문의하기 강조 ───────────── */}
{/* outer max-w-7xl로 Tier Cards 섹션과 동일 정렬 (반응형 자동 축소) */}
<section className="px-6 mb-16 max-w-7xl mx-auto">
<ContactFirstBox />
</section>
{/* ── Section 6 · Launch Promotion ───────────── */}
<section className="px-6 mb-20 max-w-7xl mx-auto">
<PromotionBanner />
</section>
{/* ── Section 7 · FAQ ─────────────────────────── */}
<section className="px-6 mb-20 max-w-7xl mx-auto">
<FAQ />
</section>
{/* ── Section 8 · Enterprise Contact ─────────── */}
<section className="px-6 max-w-7xl mx-auto">
<EnterpriseContact />
</section>
</main>
);
}