Files
jaengseung-made/app/quote/[token]/page.tsx
gahusb 441bf00b95 fix: PDF 전체 페이지 출력 + 필수 항목 40% 할인 표시
- @media print CSS 보강: html/body height auto, overflow visible, fixed 요소 숨김
- 하단 고정바에 no-print 클래스 추가
- afterprint 이벤트 리스너로 isPrinting 상태 안정적 해제
- 필수 항목 헤더에 40% 할인 배지, 소계에 정가 취소선/할인액 표시
- 합계 박스에 정가→할인가 비교 표시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 09:14:41 +09:00

575 lines
34 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.
'use client';
import { useEffect, useState } from 'react';
import { useParams, useSearchParams } 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 searchParams = useSearchParams();
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);
const [isPrinting, setIsPrinting] = useState(false);
useEffect(() => {
const handleAfterPrint = () => setIsPrinting(false);
window.addEventListener('afterprint', handleAfterPrint);
return () => window.removeEventListener('afterprint', handleAfterPrint);
}, []);
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);
// ?print=1 파라미터 시 자동 인쇄 다이얼로그
if (searchParams.get('print') === '1') {
setTimeout(() => {
setIsPrinting(true);
setTimeout(() => window.print(), 300);
}, 800);
}
})
.catch(() => setNotFound(true))
.finally(() => setLoading(false));
}, [token]);
const isExpired = quote?.valid_until ? new Date(quote.valid_until) < new Date() : false;
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 DISCOUNT_RATE = 0.4; // 40% 할인
const requiredOriginal = Math.round(requiredTotal / (1 - DISCOUNT_RATE));
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; }
@media print {
html, body { height: auto !important; min-height: 0 !important; overflow: visible !important; background: white !important; color: #1e293b !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
* { overflow: visible !important; }
.no-print { display: none !important; }
.print-break { page-break-before: always; }
.print-section-title { display: block !important; color: #1e293b !important; }
div[style] { background: white !important; color: #1e293b !important; min-height: 0 !important; height: auto !important; }
[style*="position: fixed"] { display: none !important; }
table { page-break-inside: auto; background: white !important; }
tr { page-break-inside: avoid; }
th, td { color: #1e293b !important; border-color: #e2e8f0 !important; }
span, p, h2, h3, div { color: #1e293b !important; }
}
.print-section-title { display: none; }
`}</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', display: 'flex', alignItems: 'center', gap: 10 }}>
<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>
<button
className="no-print"
onClick={() => {
setIsPrinting(true);
setTimeout(() => {
window.print();
setIsPrinting(false);
}, 500);
}}
style={{
display: 'flex', alignItems: 'center', gap: 6,
background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)',
color: '#cbd5e1', fontSize: 13, fontWeight: 600,
padding: '6px 14px', borderRadius: 8, cursor: 'pointer',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.14)'; e.currentTarget.style.color = 'white'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; e.currentTarget.style.color = '#cbd5e1'; }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
PDF
</button>
</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 className="no-print" style={{ display: isPrinting ? 'none' : '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>
{/* 만료 배너 */}
{isExpired && (
<div style={{ maxWidth: 900, margin: '0 auto', padding: '16px 24px 0' }}>
<div style={{ background: 'rgba(245,158,11,0.1)', border: '1px solid rgba(245,158,11,0.3)', borderRadius: 12, padding: '14px 20px', display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 18 }}>&#9888;</span>
<div>
<div style={{ color: '#f59e0b', fontWeight: 700, fontSize: 14 }}> </div>
<div style={{ color: '#92400e', fontSize: 13 }}>({quote.valid_until?.slice(0, 10)}) . .</div>
</div>
</div>
</div>
)}
{/* 본문 */}
<div style={{ maxWidth: 900, margin: '0 auto', padding: '32px 24px' }}>
{/* ── 개요 ── */}
{(isPrinting || activeTab === 'overview') && (
<div style={{ animation: 'fadeUp 0.4s ease' }}>
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}></h2>}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16 }}>
<StatCard label="총 필수 항목" value={requiredItems.length + '개'} sub="반드시 포함" color="#60a5fa" />
<StatCard label="총 선택 항목" value={optionalItems.length + '개'} sub="고객 선택 가능" color="#a78bfa" />
<StatCard label="필수 견적 합계" value={requiredTotal.toLocaleString() + '원'} sub={'정가 ' + requiredOriginal.toLocaleString() + '원 → 40% 할인'} color="#34d399" />
<StatCard
label="선택 포함 합계"
value={grandTotal.toLocaleString() + '원'}
sub={optionalItems.filter(i => checkedOptional[i.id]).length + '개 선택됨'}
color="#f59e0b"
/>
</div>
</div>
)}
{/* ── WBS ── */}
{(isPrinting || activeTab === 'wbs') && quote.wbs.length > 0 && (
<div style={{ animation: 'fadeUp 0.4s ease', marginTop: isPrinting ? 40 : 0 }}>
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}>WBS ( )</h2>}
{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', tableLayout: 'fixed' }}>
<colgroup>
<col style={{ width: '28%' }} />
<col style={{ width: '12%' }} />
<col style={{ width: '60%' }} />
</colgroup>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<th style={thStyle}></th>
<th style={thStyle}></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, wordBreak: 'keep-all' }}>{task.name}</td>
<td style={{ ...tdStyle, color: '#818cf8', fontWeight: 600, whiteSpace: 'nowrap' }}>{task.duration}</td>
<td style={{ ...tdStyle, color: '#64748b' }}>{task.description || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
)}
{/* ── 견적 항목 ── */}
{(isPrinting || activeTab === 'quote') && (
<div style={{ animation: 'fadeUp 0.4s ease', marginTop: isPrinting ? 40 : 0 }}>
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}> </h2>}
{/* 필수 항목 */}
{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' }} />
<span style={{ background: 'linear-gradient(135deg, #ef4444, #f97316)', color: 'white', fontSize: 11, fontWeight: 700, padding: '2px 10px', borderRadius: 100, marginLeft: 4 }}>40% </span>
</h3>
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', minWidth: 700 }}>
<colgroup>
<col style={{ width: '10%' }} />
<col style={{ width: '18%' }} />
<col style={{ width: '42%' }} />
<col style={{ width: '6%' }} />
<col style={{ width: '12%' }} />
<col style={{ width: '12%' }} />
</colgroup>
<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, whiteSpace: 'nowrap', display: 'inline-block' }}>
{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', whiteSpace: 'nowrap' }}>{item.quantity}</td>
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{item.unitPrice.toLocaleString()}</td>
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: 'white', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{(item.unitPrice * item.quantity).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 필수 항목 할인 소계 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12, gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
<span style={{ color: '#64748b', fontSize: 13, textDecoration: 'line-through', fontFamily: 'monospace' }}> {requiredOriginal.toLocaleString()}</span>
<span style={{ color: '#ef4444', fontSize: 13, fontWeight: 600 }}>{(requiredOriginal - requiredTotal).toLocaleString()} </span>
<span style={{ color: '#34d399', fontSize: 15, fontWeight: 700, fontFamily: 'monospace' }}>{requiredTotal.toLocaleString()}</span>
</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)', overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', minWidth: 700 }}>
<colgroup>
<col style={{ width: '6%' }} />
<col style={{ width: '10%' }} />
<col style={{ width: '16%' }} />
<col style={{ width: '42%' }} />
<col style={{ width: '6%' }} />
<col style={{ width: '12%' }} />
</colgroup>
<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}></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, whiteSpace: 'nowrap', display: 'inline-block' }}>
{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', whiteSpace: 'nowrap' }}>{item.quantity}</td>
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: checkedOptional[item.id] ? '#a78bfa' : '#475569', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>
{(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: 360 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ color: '#64748b', fontSize: 14 }}> </span>
<span style={{ color: '#64748b', fontSize: 13, fontFamily: 'monospace', textDecoration: 'line-through' }}>{requiredOriginal.toLocaleString()}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
<span style={{ color: '#ef4444', fontSize: 13, fontWeight: 600 }}>40% </span>
<span style={{ color: '#34d399', fontSize: 14, fontWeight: 700, 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>
)}
{/* ── 향후 관리 ── */}
{(isPrinting || activeTab === 'maintenance') && quote.maintenance.length > 0 && (
<div style={{ animation: 'fadeUp 0.4s ease', marginTop: isPrinting ? 40 : 0 }}>
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}> </h2>}
<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' && !isExpired && (
<div className="no-print" 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' };