feat: 견적서 자동화, 마케팅 에셋, 전체 카피 강화

- 관리자 견적서 CRUD (WBS/항목/향후관리/특이사항 5탭 편집기)
- 고객용 공개 견적서 페이지 (optional 항목 선택 + 실시간 총액 + 수락)
- 마케팅 SVG 에셋 6종 (썸네일 5개 + 배너 1개) + 관리자 에셋 페이지
- 전체 카피 강화: 크레덴셜 제거 → URL증거/환불보장/계약서/납기패널티 중심

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 10:48:28 +09:00
parent d29cdbcd82
commit 3f53594d3f
20 changed files with 2399 additions and 36 deletions

466
app/quote/[token]/page.tsx Normal file
View File

@@ -0,0 +1,466 @@
'use client';
import { useEffect, useState } from 'react';
import { useParams } from 'next/navigation';
interface WBSTask { id: string; name: string; duration: string; description: string; }
interface WBSPhase { id: string; phase: string; tasks: WBSTask[]; }
interface QuoteItem {
id: string; category: string; name: string; description: string;
quantity: number; unitPrice: number; optional: boolean;
}
interface MaintenancePlan {
id: string; name: string; period: string; monthlyFee: number;
includes: string[]; recommended: boolean;
}
interface Quote {
id: string; title: string; client_name: string; valid_until: string | null;
status: string; wbs: WBSPhase[]; items: QuoteItem[];
maintenance: MaintenancePlan[]; notes: string; created_at: string;
}
const CATEGORY_COLORS: Record<string, string> = {
: '#60a5fa', : '#f472b6', : '#34d399', : '#fb923c',
: '#a78bfa', : '#94a3b8',
};
export default function QuotePage() {
const { token } = useParams<{ token: string }>();
const [quote, setQuote] = useState<Quote | null>(null);
const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
// 선택 상태
const [checkedOptional, setCheckedOptional] = useState<Record<string, boolean>>({});
const [selectedMaintenance, setSelectedMaintenance] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'overview' | 'wbs' | 'quote' | 'maintenance'>('overview');
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
fetch(`/api/quote/${token}`)
.then((r) => r.ok ? r.json() : Promise.reject())
.then((d) => {
setQuote(d.quote);
// 기본값: 필수 항목은 항상 체크, 선택 항목은 기본 체크
const init: Record<string, boolean> = {};
(d.quote.items as QuoteItem[]).forEach((i) => { init[i.id] = true; });
setCheckedOptional(init);
// 추천 유지보수 플랜 기본 선택
const rec = (d.quote.maintenance as MaintenancePlan[]).find((p) => p.recommended);
if (rec) setSelectedMaintenance(rec.id);
else if (d.quote.maintenance.length > 0) setSelectedMaintenance(d.quote.maintenance[0].id);
})
.catch(() => setNotFound(true))
.finally(() => setLoading(false));
}, [token]);
const requiredItems = quote?.items.filter((i) => !i.optional) ?? [];
const optionalItems = quote?.items.filter((i) => i.optional) ?? [];
const requiredTotal = requiredItems.reduce((s, i) => s + i.unitPrice * i.quantity, 0);
const optionalTotal = optionalItems
.filter((i) => checkedOptional[i.id])
.reduce((s, i) => s + i.unitPrice * i.quantity, 0);
const selectedPlan = quote?.maintenance.find((p) => p.id === selectedMaintenance);
const maintenanceTotal = selectedPlan ? selectedPlan.monthlyFee : 0;
const grandTotal = requiredTotal + optionalTotal;
async function handleAccept() {
if (!quote) return;
setSubmitting(true);
const selectedItems = quote.items.filter((i) => !i.optional || checkedOptional[i.id]).map((i) => i.id);
await fetch(`/api/quote/${token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ selectedItems, selectedMaintenance, total: grandTotal }),
});
setSubmitting(false);
setSubmitted(true);
}
if (loading) {
return (
<div style={{ minHeight: '100vh', background: '#0a0f1e', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ textAlign: 'center' }}>
<div style={{ width: 40, height: 40, border: '3px solid rgba(99,102,241,0.3)', borderTopColor: '#6366f1', borderRadius: '50%', animation: 'spin 0.8s linear infinite', margin: '0 auto 16px' }} />
<p style={{ color: '#475569', fontFamily: 'sans-serif' }}> ...</p>
</div>
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</div>
);
}
if (notFound || !quote) {
return (
<div style={{ minHeight: '100vh', background: '#0a0f1e', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 16 }}>
<div style={{ fontSize: 64 }}>🔍</div>
<h1 style={{ color: 'white', fontSize: 24, fontWeight: 700, fontFamily: 'sans-serif' }}> </h1>
<p style={{ color: '#475569', fontFamily: 'sans-serif' }}> </p>
</div>
);
}
if (submitted) {
return (
<div style={{ minHeight: '100vh', background: '#0a0f1e', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 20, padding: 24 }}>
<style>{`@keyframes pop { 0% { transform: scale(0.5); opacity: 0; } 70% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }`}</style>
<div style={{ fontSize: 80, animation: 'pop 0.5s ease forwards' }}>🎉</div>
<h1 style={{ color: 'white', fontSize: 28, fontWeight: 800, fontFamily: 'sans-serif', textAlign: 'center' }}> !</h1>
<p style={{ color: '#94a3b8', fontFamily: 'sans-serif', textAlign: 'center', lineHeight: 1.7 }}>
.<br />
.
</p>
<div style={{ background: '#0f172a', border: '1px solid rgba(99,102,241,0.3)', borderRadius: 16, padding: '24px 32px', textAlign: 'center' }}>
<div style={{ color: '#94a3b8', fontSize: 14, fontFamily: 'sans-serif', marginBottom: 8 }}> </div>
<div style={{ color: 'white', fontSize: 36, fontWeight: 800, fontFamily: 'sans-serif' }}>{grandTotal.toLocaleString()}</div>
{maintenanceTotal > 0 && (
<div style={{ color: '#6366f1', fontSize: 14, fontFamily: 'sans-serif', marginTop: 6 }}>+ {maintenanceTotal.toLocaleString()}/</div>
)}
</div>
</div>
);
}
const tabs = [
{ key: 'overview', label: '개요' },
{ key: 'wbs', label: 'WBS', show: quote.wbs.length > 0 },
{ key: 'quote', label: '견적 항목', show: quote.items.length > 0 },
{ key: 'maintenance', label: '향후 관리', show: quote.maintenance.length > 0 },
].filter((t) => t.show !== false);
return (
<div style={{ background: '#0a0f1e', minHeight: '100vh', color: 'white', fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif" }}>
<style>{`
@keyframes fadeUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes shimmer { from { background-position: -200% 0; } to { background-position: 200% 0; } }
* { box-sizing: border-box; }
input[type=checkbox] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
input[type=radio] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
`}</style>
{/* 헤더 */}
<div style={{ background: 'linear-gradient(180deg, #0f172a 0%, #0a0f1e 100%)', borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '32px 24px 0' }}>
<div style={{ maxWidth: 900, margin: '0 auto' }}>
{/* 브랜드 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 32 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, fontWeight: 700 }}></div>
<div>
<div style={{ color: 'white', fontWeight: 700, fontSize: 14 }}></div>
<div style={{ color: '#475569', fontSize: 11 }}>jaengseung-made.com</div>
</div>
<div style={{ marginLeft: 'auto' }}>
<span style={{ background: 'rgba(99,102,241,0.15)', border: '1px solid rgba(99,102,241,0.3)', color: '#818cf8', fontSize: 11, fontWeight: 600, padding: '4px 12px', borderRadius: 100 }}>
</span>
</div>
</div>
{/* 제목 */}
<div style={{ animation: 'fadeUp 0.6s ease forwards', paddingBottom: 28 }}>
<h1 style={{ fontSize: 'clamp(22px, 4vw, 36px)', fontWeight: 800, color: 'white', marginBottom: 12, lineHeight: 1.2 }}>
{quote.title}
</h1>
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
{quote.client_name && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#94a3b8', fontSize: 14 }}>
<span>👤</span> {quote.client_name}
</div>
)}
{quote.valid_until && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#94a3b8', fontSize: 14 }}>
<span>📅</span> : {quote.valid_until.slice(0, 10)}
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#94a3b8', fontSize: 14 }}>
<span>📄</span> : {new Date(quote.created_at).toLocaleDateString('ko-KR')}
</div>
</div>
</div>
{/* 탭 */}
<div style={{ display: 'flex', gap: 0, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
{tabs.map((t) => (
<button key={t.key} onClick={() => setActiveTab(t.key as typeof activeTab)}
style={{
padding: '12px 20px', fontSize: 14, fontWeight: 500, border: 'none', cursor: 'pointer',
background: 'none', color: activeTab === t.key ? '#818cf8' : '#64748b',
borderBottom: `2px solid ${activeTab === t.key ? '#6366f1' : 'transparent'}`,
transition: 'all 0.2s', marginBottom: -1,
}}>
{t.label}
</button>
))}
</div>
</div>
</div>
{/* 본문 */}
<div style={{ maxWidth: 900, margin: '0 auto', padding: '32px 24px' }}>
{/* ── 개요 ── */}
{activeTab === 'overview' && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, animation: 'fadeUp 0.4s ease' }}>
<StatCard label="총 필수 항목" value={requiredItems.length + '개'} sub="반드시 포함" color="#60a5fa" />
<StatCard label="총 선택 항목" value={optionalItems.length + '개'} sub="고객 선택 가능" color="#a78bfa" />
<StatCard label="필수 견적 합계" value={requiredTotal.toLocaleString() + '원'} sub="VAT 별도" color="#34d399" />
<StatCard
label="선택 포함 합계"
value={grandTotal.toLocaleString() + '원'}
sub={optionalItems.filter(i => checkedOptional[i.id]).length + '개 선택됨'}
color="#f59e0b"
/>
</div>
)}
{/* ── WBS ── */}
{activeTab === 'wbs' && (
<div style={{ animation: 'fadeUp 0.4s ease' }}>
{quote.wbs.map((phase, pi) => (
<div key={phase.id} style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, flexShrink: 0 }}>
{pi + 1}
</div>
<h3 style={{ fontSize: 18, fontWeight: 700, color: 'white' }}>{phase.phase}</h3>
</div>
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<th style={thStyle}></th>
<th style={{ ...thStyle, width: 100 }}></th>
<th style={thStyle}></th>
</tr>
</thead>
<tbody>
{phase.tasks.map((task) => (
<tr key={task.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
<td style={tdStyle}>{task.name}</td>
<td style={{ ...tdStyle, color: '#818cf8', fontWeight: 600 }}>{task.duration}</td>
<td style={{ ...tdStyle, color: '#64748b' }}>{task.description || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
)}
{/* ── 견적 항목 ── */}
{activeTab === 'quote' && (
<div style={{ animation: 'fadeUp 0.4s ease' }}>
{/* 필수 항목 */}
{requiredItems.length > 0 && (
<section style={{ marginBottom: 32 }}>
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#60a5fa', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#60a5fa', display: 'inline-block' }} />
</h3>
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<th style={thStyle}></th>
<th style={thStyle}></th>
<th style={thStyle}></th>
<th style={{ ...thStyle, textAlign: 'right' }}></th>
<th style={{ ...thStyle, textAlign: 'right' }}></th>
<th style={{ ...thStyle, textAlign: 'right' }}></th>
</tr>
</thead>
<tbody>
{requiredItems.map((item) => (
<tr key={item.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
<td style={tdStyle}>
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100 }}>
{item.category}
</span>
</td>
<td style={{ ...tdStyle, fontWeight: 600, color: 'white' }}>{item.name}</td>
<td style={{ ...tdStyle, color: '#64748b' }}>{item.description || '—'}</td>
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8' }}>{item.quantity}</td>
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8', fontFamily: 'monospace' }}>{item.unitPrice.toLocaleString()}</td>
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: 'white', fontFamily: 'monospace' }}>{(item.unitPrice * item.quantity).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{/* 선택 항목 */}
{optionalItems.length > 0 && (
<section style={{ marginBottom: 32 }}>
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#a78bfa', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#a78bfa', display: 'inline-block' }} />
</h3>
<p style={{ color: '#475569', fontSize: 13, marginBottom: 12 }}> </p>
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(167,139,250,0.2)', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<th style={{ ...thStyle, width: 50 }}></th>
<th style={thStyle}></th>
<th style={thStyle}></th>
<th style={thStyle}></th>
<th style={{ ...thStyle, textAlign: 'right' }}></th>
<th style={{ ...thStyle, textAlign: 'right' }}></th>
</tr>
</thead>
<tbody>
{optionalItems.map((item) => (
<tr key={item.id}
onClick={() => setCheckedOptional((prev) => ({ ...prev, [item.id]: !prev[item.id] }))}
style={{ borderBottom: '1px solid rgba(255,255,255,0.04)', cursor: 'pointer', background: checkedOptional[item.id] ? 'rgba(167,139,250,0.05)' : 'transparent', transition: 'background 0.2s' }}>
<td style={{ ...tdStyle, textAlign: 'center' }}>
<input type="checkbox" checked={!!checkedOptional[item.id]} onChange={() => {}} />
</td>
<td style={tdStyle}>
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100 }}>
{item.category}
</span>
</td>
<td style={{ ...tdStyle, fontWeight: 600, color: checkedOptional[item.id] ? 'white' : '#64748b' }}>{item.name}</td>
<td style={{ ...tdStyle, color: '#475569' }}>{item.description || '—'}</td>
<td style={{ ...tdStyle, textAlign: 'right', color: '#64748b' }}>{item.quantity}</td>
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: checkedOptional[item.id] ? '#a78bfa' : '#475569', fontFamily: 'monospace' }}>
{(item.unitPrice * item.quantity).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{/* 합계 */}
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ background: '#0f172a', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, padding: '24px 28px', width: 320 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
<span style={{ color: '#64748b', fontSize: 14 }}> </span>
<span style={{ color: '#94a3b8', fontSize: 14, fontFamily: 'monospace' }}>{requiredTotal.toLocaleString()}</span>
</div>
{optionalTotal > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
<span style={{ color: '#64748b', fontSize: 14 }}> </span>
<span style={{ color: '#a78bfa', fontSize: 14, fontFamily: 'monospace' }}>+{optionalTotal.toLocaleString()}</span>
</div>
)}
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)', paddingTop: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<span style={{ color: 'white', fontWeight: 700, fontSize: 16 }}> (VAT )</span>
<span style={{ color: 'white', fontWeight: 800, fontSize: 24, fontFamily: 'monospace' }}>{grandTotal.toLocaleString()}</span>
</div>
</div>
</div>
</div>
)}
{/* ── 향후 관리 ── */}
{activeTab === 'maintenance' && (
<div style={{ animation: 'fadeUp 0.4s ease' }}>
<p style={{ color: '#64748b', fontSize: 14, marginBottom: 20 }}> ( )</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 16 }}>
{quote.maintenance.map((plan) => {
const isSelected = selectedMaintenance === plan.id;
return (
<div key={plan.id} onClick={() => setSelectedMaintenance(isSelected ? null : plan.id)}
style={{
background: isSelected ? 'linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.1))' : '#0f172a',
border: `1px solid ${isSelected ? '#6366f1' : 'rgba(255,255,255,0.06)'}`,
borderRadius: 16, padding: 24, cursor: 'pointer', transition: 'all 0.25s', position: 'relative',
}}>
{plan.recommended && (
<div style={{ position: 'absolute', top: 16, right: 16, background: '#6366f1', color: 'white', fontSize: 10, fontWeight: 700, padding: '3px 10px', borderRadius: 100 }}></div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
<input type="radio" checked={isSelected} onChange={() => {}} />
<div>
<div style={{ color: 'white', fontWeight: 700, fontSize: 16 }}>{plan.name}</div>
<div style={{ color: '#475569', fontSize: 13 }}>{plan.period}</div>
</div>
</div>
<div style={{ fontSize: 24, fontWeight: 800, color: isSelected ? '#818cf8' : 'white', marginBottom: 16, fontFamily: 'monospace' }}>
{plan.monthlyFee === 0 ? '무료' : plan.monthlyFee.toLocaleString() + '원/월'}
</div>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
{plan.includes.map((inc, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, fontSize: 13, color: '#94a3b8' }}>
<span style={{ color: '#6366f1', flexShrink: 0, marginTop: 1 }}></span>
{inc}
</div>
))}
</div>
</div>
);
})}
</div>
</div>
)}
{/* 특이사항 */}
{quote.notes && (
<div style={{ marginTop: 40, background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', padding: 24 }}>
<h3 style={{ fontSize: 14, fontWeight: 700, color: '#475569', marginBottom: 12, textTransform: 'uppercase', letterSpacing: '0.1em' }}> </h3>
<p style={{ color: '#64748b', fontSize: 14, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>{quote.notes}</p>
</div>
)}
</div>
{/* 하단 고정 바 — 견적 수락 */}
{quote.status !== 'accepted' && quote.status !== 'rejected' && (
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, background: 'rgba(10,15,30,0.95)', backdropFilter: 'blur(12px)', borderTop: '1px solid rgba(255,255,255,0.08)', padding: '16px 24px' }}>
<div style={{ maxWidth: 900, margin: '0 auto', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
<div>
<div style={{ color: '#64748b', fontSize: 13 }}> </div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span style={{ color: 'white', fontSize: 24, fontWeight: 800, fontFamily: 'monospace' }}>{grandTotal.toLocaleString()}</span>
{maintenanceTotal > 0 && selectedPlan && (
<span style={{ color: '#6366f1', fontSize: 13 }}>+ {maintenanceTotal.toLocaleString()}/ ({selectedPlan.name})</span>
)}
</div>
</div>
<button onClick={handleAccept} disabled={submitting}
style={{
padding: '14px 36px', borderRadius: 12, border: 'none', cursor: 'pointer',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
color: 'white', fontSize: 16, fontWeight: 700, transition: 'all 0.2s',
boxShadow: '0 8px 32px rgba(99,102,241,0.4)',
opacity: submitting ? 0.7 : 1,
}}>
{submitting ? '처리 중...' : '이 견적으로 진행하겠습니다 →'}
</button>
</div>
</div>
)}
{/* 수락된 경우 */}
{quote.status === 'accepted' && (
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, background: 'rgba(16,185,129,0.1)', backdropFilter: 'blur(12px)', borderTop: '1px solid rgba(16,185,129,0.3)', padding: '16px 24px', textAlign: 'center' }}>
<p style={{ color: '#34d399', fontWeight: 600, fontSize: 16 }}> </p>
</div>
)}
{/* 하단 여백 */}
<div style={{ height: 80 }} />
</div>
);
}
function StatCard({ label, value, sub, color }: { label: string; value: string; sub: string; color: string }) {
return (
<div style={{ background: '#0f172a', border: `1px solid ${color}20`, borderRadius: 16, padding: 24 }}>
<div style={{ color: '#475569', fontSize: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 10 }}>{label}</div>
<div style={{ color, fontSize: 28, fontWeight: 800, fontFamily: 'monospace', marginBottom: 4 }}>{value}</div>
<div style={{ color: '#374151', fontSize: 12 }}>{sub}</div>
</div>
);
}
const thStyle: React.CSSProperties = { padding: '12px 16px', textAlign: 'left', fontSize: 11, fontWeight: 600, color: '#475569', textTransform: 'uppercase', letterSpacing: '0.08em' };
const tdStyle: React.CSSProperties = { padding: '14px 16px', fontSize: 14, color: '#94a3b8' };