fix: 견적서 PDF 저장 시 전체 섹션 출력 (개요+WBS+견적+관리)

- isPrinting 상태로 인쇄 모드 전환 시 모든 탭 섹션 동시 렌더링
- 각 섹션에 인쇄용 제목 구분선 추가
- 탭 바 인쇄 시 숨김
- 테이블 행 페이지 분리 방지 (page-break-inside: avoid)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 08:56:57 +09:00
parent fae92940e5
commit f962a04468

View File

@@ -37,6 +37,7 @@ export default function QuotePage() {
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(() => {
fetch(`/api/quote/${token}`)
@@ -53,7 +54,10 @@ export default function QuotePage() {
else if (d.quote.maintenance.length > 0) setSelectedMaintenance(d.quote.maintenance[0].id);
// ?print=1 파라미터 시 자동 인쇄 다이얼로그
if (searchParams.get('print') === '1') {
setTimeout(() => window.print(), 800);
setTimeout(() => {
setIsPrinting(true);
setTimeout(() => window.print(), 300);
}, 800);
}
})
.catch(() => setNotFound(true))
@@ -145,14 +149,18 @@ export default function QuotePage() {
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 {
body { background: white !important; color: #1e293b !important; }
body { background: white !important; color: #1e293b !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.no-print { display: none !important; }
.print-break { page-break-before: always; }
.print-section-title { display: block !important; }
[style*="background: #0a0f1e"], [style*="background: #0f172a"] {
background: white !important;
color: #1e293b !important;
}
table { page-break-inside: auto; }
tr { page-break-inside: avoid; }
}
.print-section-title { display: none; }
`}</style>
{/* 헤더 */}
@@ -171,7 +179,13 @@ export default function QuotePage() {
</span>
<button
className="no-print"
onClick={() => window.print()}
onClick={() => {
setIsPrinting(true);
setTimeout(() => {
window.print();
setIsPrinting(false);
}, 300);
}}
style={{
display: 'flex', alignItems: 'center', gap: 6,
background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)',
@@ -213,7 +227,7 @@ export default function QuotePage() {
</div>
{/* 탭 */}
<div style={{ display: 'flex', gap: 0, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<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={{
@@ -246,8 +260,10 @@ export default function QuotePage() {
<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' }}>
{(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="VAT 별도" color="#34d399" />
@@ -258,11 +274,13 @@ export default function QuotePage() {
color="#f59e0b"
/>
</div>
</div>
)}
{/* ── WBS ── */}
{activeTab === 'wbs' && (
<div style={{ animation: 'fadeUp 0.4s ease' }}>
{(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 }}>
@@ -302,8 +320,9 @@ export default function QuotePage() {
)}
{/* ── 견적 항목 ── */}
{activeTab === 'quote' && (
<div style={{ animation: 'fadeUp 0.4s ease' }}>
{(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 }}>
@@ -430,8 +449,9 @@ export default function QuotePage() {
)}
{/* ── 향후 관리 ── */}
{activeTab === 'maintenance' && (
<div style={{ animation: 'fadeUp 0.4s ease' }}>
{(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) => {