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 [activeTab, setActiveTab] = useState<'overview' | 'wbs' | 'quote' | 'maintenance'>('overview');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const [isPrinting, setIsPrinting] = useState(false);
useEffect(() => { useEffect(() => {
fetch(`/api/quote/${token}`) 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); else if (d.quote.maintenance.length > 0) setSelectedMaintenance(d.quote.maintenance[0].id);
// ?print=1 파라미터 시 자동 인쇄 다이얼로그 // ?print=1 파라미터 시 자동 인쇄 다이얼로그
if (searchParams.get('print') === '1') { if (searchParams.get('print') === '1') {
setTimeout(() => window.print(), 800); setTimeout(() => {
setIsPrinting(true);
setTimeout(() => window.print(), 300);
}, 800);
} }
}) })
.catch(() => setNotFound(true)) .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=checkbox] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
input[type=radio] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; } input[type=radio] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
@media print { @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; } .no-print { display: none !important; }
.print-break { page-break-before: always; } .print-break { page-break-before: always; }
.print-section-title { display: block !important; }
[style*="background: #0a0f1e"], [style*="background: #0f172a"] { [style*="background: #0a0f1e"], [style*="background: #0f172a"] {
background: white !important; background: white !important;
color: #1e293b !important; color: #1e293b !important;
} }
table { page-break-inside: auto; }
tr { page-break-inside: avoid; }
} }
.print-section-title { display: none; }
`}</style> `}</style>
{/* 헤더 */} {/* 헤더 */}
@@ -171,7 +179,13 @@ export default function QuotePage() {
</span> </span>
<button <button
className="no-print" className="no-print"
onClick={() => window.print()} onClick={() => {
setIsPrinting(true);
setTimeout(() => {
window.print();
setIsPrinting(false);
}, 300);
}}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', 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>
{/* 탭 */} {/* 탭 */}
<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) => ( {tabs.map((t) => (
<button key={t.key} onClick={() => setActiveTab(t.key as typeof activeTab)} <button key={t.key} onClick={() => setActiveTab(t.key as typeof activeTab)}
style={{ style={{
@@ -246,8 +260,10 @@ export default function QuotePage() {
<div style={{ maxWidth: 900, margin: '0 auto', padding: '32px 24px' }}> <div style={{ maxWidth: 900, margin: '0 auto', padding: '32px 24px' }}>
{/* ── 개요 ── */} {/* ── 개요 ── */}
{activeTab === 'overview' && ( {(isPrinting || activeTab === 'overview') && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, animation: 'fadeUp 0.4s ease' }}> <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={requiredItems.length + '개'} sub="반드시 포함" color="#60a5fa" />
<StatCard label="총 선택 항목" value={optionalItems.length + '개'} sub="고객 선택 가능" color="#a78bfa" /> <StatCard label="총 선택 항목" value={optionalItems.length + '개'} sub="고객 선택 가능" color="#a78bfa" />
<StatCard label="필수 견적 합계" value={requiredTotal.toLocaleString() + '원'} sub="VAT 별도" color="#34d399" /> <StatCard label="필수 견적 합계" value={requiredTotal.toLocaleString() + '원'} sub="VAT 별도" color="#34d399" />
@@ -258,11 +274,13 @@ export default function QuotePage() {
color="#f59e0b" color="#f59e0b"
/> />
</div> </div>
</div>
)} )}
{/* ── WBS ── */} {/* ── WBS ── */}
{activeTab === 'wbs' && ( {(isPrinting || activeTab === 'wbs') && quote.wbs.length > 0 && (
<div style={{ animation: 'fadeUp 0.4s ease' }}> <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) => ( {quote.wbs.map((phase, pi) => (
<div key={phase.id} style={{ marginBottom: 24 }}> <div key={phase.id} style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
@@ -302,8 +320,9 @@ export default function QuotePage() {
)} )}
{/* ── 견적 항목 ── */} {/* ── 견적 항목 ── */}
{activeTab === 'quote' && ( {(isPrinting || activeTab === 'quote') && (
<div style={{ animation: 'fadeUp 0.4s ease' }}> <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 && ( {requiredItems.length > 0 && (
<section style={{ marginBottom: 32 }}> <section style={{ marginBottom: 32 }}>
@@ -430,8 +449,9 @@ export default function QuotePage() {
)} )}
{/* ── 향후 관리 ── */} {/* ── 향후 관리 ── */}
{activeTab === 'maintenance' && ( {(isPrinting || activeTab === 'maintenance') && quote.maintenance.length > 0 && (
<div style={{ animation: 'fadeUp 0.4s ease' }}> <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> <p style={{ color: '#64748b', fontSize: 14, marginBottom: 20 }}> ( )</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 16 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 16 }}>
{quote.maintenance.map((plan) => { {quote.maintenance.map((plan) => {