feat: 로또 서비스 플랜별 기능 차별화 및 서비스화

- 플랜 등급 유틸리티 (PLAN_RANK, planGte) 추가
- 탭 네비게이션: 골드 미만 → 공략/구매 탭 🔒 잠금, 플래티넘 미만 → 패턴 탭 💎 PLATINUM+ 뱃지
- 내 패턴 탭: 플래티넘 미만 접속 시 업그레이드 유도 카드 표시
- 비구독자 업셀 UI 전면 개편: 잠금 오버레이 → 골드/플래티넘/다이아 플랜 기능 미리보기 카드
- 로또 서비스 소개 페이지: 9개 기능 행 x 3개 플랜 비교표 추가 (요금제 섹션 위)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 01:29:57 +09:00
parent 4cacea69c8
commit 54d252372b
2 changed files with 208 additions and 47 deletions

View File

@@ -221,6 +221,65 @@ export default function LottoPage() {
</div> </div>
</div> </div>
{/* ─── 기능 비교표 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8">
<p className="text-amber-600 text-xs font-bold uppercase tracking-widest mb-2">PLAN FEATURES</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
<p className="text-slate-500 text-sm mt-2"> </p>
</div>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="text-left py-3 px-4 text-slate-500 text-sm font-semibold"></th>
<th className="text-center py-3 px-4">
<div className="text-amber-600 font-bold text-sm">🥇 </div>
<div className="text-slate-400 text-xs font-normal">900/</div>
</th>
<th className="text-center py-3 px-4 bg-gradient-to-b from-amber-50 to-orange-50 rounded-t-xl">
<div className="text-orange-600 font-bold text-sm">💎 </div>
<div className="text-slate-400 text-xs font-normal">2,900/</div>
<div className="text-orange-500 text-xs font-bold mt-1"></div>
</th>
<th className="text-center py-3 px-4">
<div className="text-blue-600 font-bold text-sm">👑 </div>
<div className="text-slate-400 text-xs font-normal">9,900/</div>
</th>
</tr>
</thead>
<tbody>
{[
{ feature: '번호 생성 횟수', gold: '1세트/일', plat: '3세트/일', dia: '무제한' },
{ feature: '배치 생성 (동시)', gold: '—', plat: '—', dia: '5개 동시' },
{ feature: '전략 선택 (균형/고위험/안정)', gold: '✓', plat: '✓', dia: '✓' },
{ feature: '이번 주 공략 리포트', gold: '✓', plat: '✓', dia: '✓' },
{ feature: '핫/콜드 번호 분석', gold: '✓', plat: '✓', dia: '✓' },
{ feature: '구매 기록 관리', gold: '✓', plat: '✓', dia: '✓' },
{ feature: '내 패턴 AI 분석', gold: '—', plat: '✓', dia: '✓' },
{ feature: '연간 당첨 패턴 리포트', gold: '—', plat: '—', dia: '✓' },
{ feature: '우선 고객 지원', gold: '—', plat: '—', dia: '✓' },
].map((row, i) => (
<tr key={row.feature} className={i % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
<td className="py-3 px-4 text-slate-700 text-sm font-medium">{row.feature}</td>
<td className="text-center py-3 px-4 text-sm">
<span className={row.gold === '—' ? 'text-slate-300' : 'text-emerald-600 font-bold'}>{row.gold}</span>
</td>
<td className="text-center py-3 px-4 text-sm bg-amber-50/50">
<span className={row.plat === '—' ? 'text-slate-300' : 'text-emerald-600 font-bold'}>{row.plat}</span>
</td>
<td className="text-center py-3 px-4 text-sm">
<span className={row.dia === '—' ? 'text-slate-300' : 'text-emerald-600 font-bold'}>{row.dia}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* ─── 요금제 ─── */} {/* ─── 요금제 ─── */}
<div className="px-6 pb-12 lg:px-12"> <div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">

View File

@@ -157,6 +157,18 @@ const PLAN_MAX_COMBOS: Record<string, number> = {
lotto_diamond: 999, lotto_diamond: 999,
}; };
const PLAN_RANK: Record<string, number> = { lotto_gold: 1, lotto_platinum: 2, lotto_diamond: 3 };
function planGte(userPlan: string, required: string): boolean {
return (PLAN_RANK[userPlan] ?? 0) >= (PLAN_RANK[required] ?? 1);
}
const TABS_CONFIG = [
{ key: 'generate' as const, label: '🎲 번호 생성', requiredPlan: null as string | null, planBadge: null as string | null },
{ key: 'report' as const, label: '📋 이번 주 공략', requiredPlan: 'lotto_gold' as string | null, planBadge: '🥇 GOLD+' as string | null },
{ key: 'purchase' as const, label: '💰 구매 기록', requiredPlan: 'lotto_gold' as string | null, planBadge: '🥇 GOLD+' as string | null },
{ key: 'pattern' as const, label: '🔍 내 패턴', requiredPlan: 'lotto_platinum' as string | null, planBadge: '💎 PLATINUM+' as string | null },
];
// ─── Lotto Ball ─────────────────────────────────────────────────────────────── // ─── Lotto Ball ───────────────────────────────────────────────────────────────
function getBallStyle(n: number): { bg: string; shadow: string; text: string } { function getBallStyle(n: number): { bg: string; shadow: string; text: string } {
@@ -566,31 +578,75 @@ export default function LottoRecommendPage() {
{/* ── 탭 네비게이션 ── */} {/* ── 탭 네비게이션 ── */}
{isSubscribed && ( {isSubscribed && (
<div style={{ display: 'flex', gap: '.3rem', marginBottom: '1.5rem', background: 'rgba(255,255,255,.04)', borderRadius: '.75rem', padding: '.3rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '.3rem', marginBottom: '1.5rem', background: 'rgba(255,255,255,.04)', borderRadius: '.75rem', padding: '.3rem', flexWrap: 'wrap' }}>
{([ {TABS_CONFIG.map(tab => {
{ key: 'generate', label: '🎲 번호 생성' }, const isLocked = tab.requiredPlan !== null && !planGte(plan, tab.requiredPlan);
{ key: 'report', label: '📋 이번 주 공략' }, const isActive = activeTab === tab.key;
{ key: 'purchase', label: '💰 구매 기록' }, return (
{ key: 'pattern', label: '🔍 내 패턴' }, <button
] as const).map(tab => ( key={tab.key}
<button key={tab.key} onClick={() => setActiveTab(tab.key)} onClick={() => {
if (isLocked) return;
setActiveTab(tab.key);
}}
style={{ style={{
flex: 1, minWidth: 80, flex: 1, minWidth: 80, position: 'relative',
background: activeTab === tab.key ? 'rgba(251,191,36,.15)' : 'transparent', background: isLocked ? 'rgba(255,255,255,.02)' : isActive ? 'rgba(251,191,36,.15)' : 'transparent',
border: `1px solid ${activeTab === tab.key ? 'rgba(251,191,36,.35)' : 'transparent'}`, border: `1px solid ${isLocked ? 'rgba(255,255,255,.05)' : isActive ? 'rgba(251,191,36,.35)' : 'transparent'}`,
color: activeTab === tab.key ? '#fbbf24' : 'rgba(255,255,255,.35)', color: isLocked ? 'rgba(255,255,255,.2)' : isActive ? '#fbbf24' : 'rgba(255,255,255,.35)',
borderRadius: '.5rem', padding: '.5rem .5rem', fontSize: '.72rem', fontWeight: 700, borderRadius: '.5rem', padding: '.5rem .5rem', fontSize: '.72rem', fontWeight: 700,
cursor: 'pointer', transition: 'all .2s ease', whiteSpace: 'nowrap', cursor: isLocked ? 'not-allowed' : 'pointer', transition: 'all .2s ease', whiteSpace: 'nowrap',
}}
>
{isLocked ? '🔒 ' : ''}{tab.label}
{isLocked && tab.planBadge && (
<div style={{
position: 'absolute', top: -8, right: -4,
background: tab.requiredPlan === 'lotto_platinum' ? 'linear-gradient(135deg,#a78bfa,#7c3aed)' : 'linear-gradient(135deg,#fbbf24,#d97706)',
color: tab.requiredPlan === 'lotto_platinum' ? '#fff' : '#78350f',
fontSize: '.45rem', fontWeight: 800, padding: '.15rem .4rem', borderRadius: '2rem',
letterSpacing: '.06em', whiteSpace: 'nowrap',
}}> }}>
{tab.label} {tab.planBadge}
</div>
)}
</button> </button>
))} );
})}
</div> </div>
)} )}
{/* ── 탭별 컨텐츠: 공략/구매/패턴 ── */} {/* ── 탭별 컨텐츠: 공략/구매/패턴 ── */}
{isSubscribed && activeTab === 'report' && <ReportTab />} {isSubscribed && activeTab === 'report' && <ReportTab />}
{isSubscribed && activeTab === 'purchase' && <PurchaseTab />} {isSubscribed && activeTab === 'purchase' && <PurchaseTab />}
{isSubscribed && activeTab === 'pattern' && <PatternTab />} {isSubscribed && activeTab === 'pattern' && (
planGte(plan, 'lotto_platinum') ? (
<PatternTab />
) : (
// 플래티넘 미만 → 업셀 카드
<div style={{ background: 'linear-gradient(145deg,rgba(167,139,250,.08),rgba(124,58,237,.04))', border: '1px solid rgba(167,139,250,.2)', borderRadius: '1.5rem', padding: '3rem 2rem', textAlign: 'center' }}>
<div style={{ width: 72, height: 72, borderRadius: '50%', background: 'linear-gradient(135deg,rgba(167,139,250,.2),rgba(124,58,237,.1))', border: '1px solid rgba(167,139,250,.3)', margin: '0 auto 1.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '2rem' }}>
🔍
</div>
<div style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.6rem', letterSpacing: '.18em', color: 'rgba(167,139,250,.5)', marginBottom: '.75rem' }}>PLATINUM PLAN REQUIRED</div>
<h3 style={{ color: '#a78bfa', fontSize: '1.2rem', fontWeight: 900, margin: '0 0 .75rem' }}> </h3>
<p style={{ color: 'rgba(255,255,255,.35)', fontSize: '.82rem', lineHeight: 1.7, margin: '0 0 .75rem', maxWidth: 360, marginLeft: 'auto', marginRight: 'auto' }}>
, , AI가 .<br />
.
</p>
<div style={{ display: 'flex', gap: '.5rem', justifyContent: 'center', flexWrap: 'wrap', marginBottom: '1.5rem' }}>
{['내 번호 빈도 분석', '기피 번호 감지', '구간 분포 히트맵', '당첨 패턴 비교'].map(f => (
<span key={f} style={{ background: 'rgba(167,139,250,.1)', border: '1px solid rgba(167,139,250,.2)', borderRadius: '2rem', padding: '.3rem .8rem', color: 'rgba(167,139,250,.7)', fontSize: '.7rem', fontWeight: 600 }}>{f}</span>
))}
</div>
<a href="/services/lotto" style={{ display: 'inline-flex', alignItems: 'center', gap: '.5rem', background: 'linear-gradient(135deg,#a78bfa,#7c3aed)', color: '#fff', padding: '.85rem 2rem', borderRadius: '1rem', fontWeight: 800, fontSize: '.9rem', textDecoration: 'none', boxShadow: '0 4px 24px rgba(124,58,237,.4)' }}>
💎
</a>
<p style={{ color: 'rgba(255,255,255,.18)', fontSize: '.7rem', marginTop: '.75rem' }}>
: {PLAN_LABELS[plan] ?? plan} · 2,900/
</p>
</div>
)
)}
{/* ── 기존 메인 콘텐츠 (번호 생성 탭 or 비구독) ── */} {/* ── 기존 메인 콘텐츠 (번호 생성 탭 or 비구독) ── */}
<div style={{ display: isSubscribed && activeTab !== 'generate' ? 'none' : 'block' }}> <div style={{ display: isSubscribed && activeTab !== 'generate' ? 'none' : 'block' }}>
@@ -831,7 +887,7 @@ export default function LottoRecommendPage() {
</div> </div>
{/* 프리미엄 컨텐츠 */} {/* 프리미엄 컨텐츠 */}
<div style={{ position: 'relative', filter: isSubscribed ? 'none' : 'blur(4px)', opacity: isSubscribed ? 1 : 0.45, pointerEvents: isSubscribed ? 'auto' : 'none', transition: 'filter .3s,opacity .3s', userSelect: isSubscribed ? 'auto' : 'none' }}> <div style={{ position: 'relative', filter: isSubscribed ? 'none' : 'blur(6px)', opacity: isSubscribed ? 1 : 0.25, pointerEvents: isSubscribed ? 'auto' : 'none', transition: 'filter .3s,opacity .3s', userSelect: isSubscribed ? 'auto' : 'none' }}>
{/* ── 번호 생성 메인 카드 ── */} {/* ── 번호 생성 메인 카드 ── */}
<div style={{ background: 'linear-gradient(145deg,rgba(255,255,255,.04),rgba(251,191,36,.02))', border: '1px solid rgba(251,191,36,.15)', borderRadius: '1.75rem', padding: '2rem', marginBottom: '1.25rem', position: 'relative', overflow: 'hidden' }}> <div style={{ background: 'linear-gradient(145deg,rgba(255,255,255,.04),rgba(251,191,36,.02))', border: '1px solid rgba(251,191,36,.15)', borderRadius: '1.75rem', padding: '2rem', marginBottom: '1.25rem', position: 'relative', overflow: 'hidden' }}>
@@ -1130,34 +1186,80 @@ export default function LottoRecommendPage() {
)} )}
</div> </div>
{/* ── 비구독자 잠금 오버레이 ── */} </div>
{/* ── 비구독자 업셀 블록 ── */}
{!isSubscribed && ( {!isSubscribed && (
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10, padding: '1rem' }}> <div style={{ marginTop: '2rem' }}>
<div style={{ textAlign: 'center', maxWidth: 420, background: 'linear-gradient(145deg,rgba(4,16,43,.92),rgba(10,5,0,.88))', backdropFilter: 'blur(12px)', border: '1px solid rgba(251,191,36,.22)', borderRadius: '1.75rem', padding: '2.5rem 2rem', boxShadow: '0 32px 80px rgba(0,0,0,.6)' }}> {/* 구분선 */}
<div style={{ width: 64, height: 64, borderRadius: '50%', background: 'linear-gradient(135deg,rgba(251,191,36,.18),rgba(249,115,22,.08))', border: '1px solid rgba(251,191,36,.28)', margin: '0 auto 1.25rem', display: 'flex', alignItems: 'center', justifyContent: 'center', animation: 'pulse 3s ease-in-out infinite' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem' }}>
<svg width={28} height={28} viewBox="0 0 24 24" fill="none" stroke="#fbbf24" strokeWidth={2}> <div style={{ flex: 1, height: 1, background: 'linear-gradient(90deg,transparent,rgba(251,191,36,.2),transparent)' }} />
<rect x="3" y="11" width="18" height="11" rx="2" /> <span style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.65rem', letterSpacing: '.15em', color: 'rgba(251,191,36,.4)', textTransform: 'uppercase' }}> </span>
<path d="M7 11V7a5 5 0 0 1 10 0v4" /> <div style={{ flex: 1, height: 1, background: 'linear-gradient(90deg,transparent,rgba(251,191,36,.2),transparent)' }} />
</svg>
</div> </div>
<div style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.6rem', letterSpacing: '.18em', color: 'rgba(251,191,36,.4)', marginBottom: '.75rem' }}>SUBSCRIPTION REQUIRED</div>
<h3 style={{ color: '#fbbf24', fontSize: '1.15rem', fontWeight: 900, margin: '0 0 .625rem', letterSpacing: '-.01em' }}> </h3> {/* 플랜별 기능 미리보기 카드 3개 */}
<p style={{ color: 'rgba(253,230,138,.45)', fontSize: '.8rem', lineHeight: 1.7, margin: '0 0 1.5rem' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(220px,1fr))', gap: '1rem', marginBottom: '1.5rem' }}>
<strong style={{ color: '#fbbf24' }}></strong> 1 · <strong style={{ color: '#fbbf24' }}></strong> 3 · <strong style={{ color: '#fbbf24' }}></strong> <br /> {[
/ · · {
</p> plan: '🥇 골드 플랜',
<div style={{ display: 'flex', gap: '.75rem', justifyContent: 'center', flexWrap: 'wrap' }}> price: '900원/월',
<Link href="/services/lotto" style={{ display: 'inline-flex', alignItems: 'center', gap: '.4rem', background: 'linear-gradient(135deg,#fbbf24,#f59e0b)', color: '#78350f', padding: '.8rem 1.75rem', borderRadius: '.875rem', fontWeight: 800, fontSize: '.88rem', textDecoration: 'none', boxShadow: '0 4px 24px rgba(251,191,36,.3)' }}> color: '#fbbf24',
colorAlpha: 'rgba(251,191,36,.12)',
</Link> borderColor: 'rgba(251,191,36,.25)',
<Link href="/login" style={{ display: 'inline-flex', alignItems: 'center', background: 'rgba(255,255,255,.05)', border: '1px solid rgba(255,255,255,.1)', color: 'rgba(255,255,255,.45)', padding: '.8rem 1.25rem', borderRadius: '.875rem', fontWeight: 600, fontSize: '.88rem', textDecoration: 'none' }}> features: ['번호 1세트/일 생성', '이번 주 공략 리포트', '핫/콜드 번호 분석', '구매 기록 관리'],
cta: '골드 시작하기',
</Link> recommended: false,
},
{
plan: '💎 플래티넘 플랜',
price: '2,900원/월',
color: '#a78bfa',
colorAlpha: 'rgba(167,139,250,.12)',
borderColor: 'rgba(167,139,250,.3)',
features: ['번호 3세트/일 생성', '전략별 생성 (균형/고위험/안정)', '내 패턴 AI 분석', '상세 통계 대시보드'],
cta: '플래티넘 시작하기',
recommended: true,
},
{
plan: '👑 다이아 플랜',
price: '9,900원/월',
color: '#38bdf8',
colorAlpha: 'rgba(56,189,248,.1)',
borderColor: 'rgba(56,189,248,.25)',
features: ['번호 무제한 생성', '배치 생성 (5개 동시)', '연간 당첨 패턴 리포트', '우선 고객 지원'],
cta: '다이아 시작하기',
recommended: false,
},
].map(p => (
<div key={p.plan} style={{ background: p.colorAlpha, border: `1px solid ${p.borderColor}`, borderRadius: '1.25rem', padding: '1.25rem', position: 'relative', overflow: 'hidden' }}>
{p.recommended && (
<div style={{ position: 'absolute', top: 10, right: 10, background: p.color, color: '#fff', fontSize: '.5rem', fontWeight: 800, padding: '.2rem .5rem', borderRadius: '2rem', letterSpacing: '.08em' }}></div>
)}
<div style={{ color: p.color, fontSize: '.85rem', fontWeight: 800, marginBottom: '.25rem', fontFamily: "'Noto Sans KR',sans-serif" }}>{p.plan}</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.72rem', marginBottom: '1rem', fontFamily: "'JetBrains Mono',monospace" }}>{p.price}</div>
<ul style={{ listStyle: 'none', margin: 0, padding: 0, marginBottom: '1.25rem' }}>
{p.features.map(f => (
<li key={f} style={{ display: 'flex', alignItems: 'center', gap: '.5rem', marginBottom: '.4rem' }}>
<div style={{ width: 4, height: 4, borderRadius: '50%', background: p.color, flexShrink: 0 }} />
<span style={{ color: 'rgba(255,255,255,.4)', fontSize: '.72rem' }}>{f}</span>
</li>
))}
</ul>
<a href="/services/lotto" style={{ display: 'block', textAlign: 'center', background: p.colorAlpha, border: `1px solid ${p.borderColor}`, color: p.color, padding: '.6rem', borderRadius: '.75rem', fontWeight: 700, fontSize: '.75rem', textDecoration: 'none', transition: 'all .2s' }}>
{p.cta}
</a>
</div> </div>
))}
</div>
<div style={{ textAlign: 'center' }}>
<a href="/login" style={{ color: 'rgba(255,255,255,.25)', fontSize: '.75rem', textDecoration: 'none' }}>
?
</a>
</div> </div>
</div> </div>
)} )}
</div>
{/* ── 소셜 증거 패널 ── */} {/* ── 소셜 증거 패널 ── */}
<div style={{ background: 'rgba(255,255,255,.02)', border: '1px solid rgba(255,255,255,.04)', borderRadius: '1rem', padding: '1rem 1.5rem', marginTop: '1.75rem', display: 'flex', gap: '1.5rem', flexWrap: 'wrap', alignItems: 'center' }}> <div style={{ background: 'rgba(255,255,255,.02)', border: '1px solid rgba(255,255,255,.04)', borderRadius: '1rem', padding: '1rem 1.5rem', marginTop: '1.75rem', display: 'flex', gap: '1.5rem', flexWrap: 'wrap', alignItems: 'center' }}>