388 lines
7.6 KiB
Markdown
388 lines
7.6 KiB
Markdown
---
|
||
id: frontend-print-stylesheet
|
||
title: Print Stylesheet — @page / 인쇄 / PDF
|
||
category: Coding
|
||
status: draft
|
||
source_trust_level: B
|
||
verification_status: conceptual
|
||
created_at: 2026-05-09
|
||
updated_at: 2026-05-09
|
||
tags: [frontend, css, print, vibe-coding]
|
||
tech_stack: { language: "CSS", applicable_to: ["Frontend"] }
|
||
applied_in: []
|
||
aliases: [@page, @media print, page-break, print preview, paged.js, PDF generation]
|
||
---
|
||
|
||
# Print Stylesheet
|
||
|
||
> Web → 인쇄 / PDF. **`@media print` + `@page`**. Header / footer 반복, page break, 흑백, link URL 표시. Receipt / report / 영수증 / book.
|
||
|
||
## 📖 핵심 개념
|
||
- @media print: 인쇄용 styling.
|
||
- @page: 페이지 size / margin.
|
||
- page-break: 강제 break.
|
||
- print preview: 디버깅.
|
||
|
||
## 💻 코드 패턴
|
||
|
||
### 기본
|
||
```css
|
||
@media print {
|
||
/* Hide UI */
|
||
.header, .nav, .footer, .ads, .no-print {
|
||
display: none !important;
|
||
}
|
||
|
||
body {
|
||
font-size: 12pt;
|
||
line-height: 1.5;
|
||
color: black;
|
||
background: white;
|
||
}
|
||
|
||
/* Link URL 표시 */
|
||
a[href]::after {
|
||
content: " (" attr(href) ")";
|
||
font-size: 10pt;
|
||
color: #555;
|
||
}
|
||
|
||
/* Internal link 제외 */
|
||
a[href^="#"]::after,
|
||
a[href^="javascript:"]::after {
|
||
content: "";
|
||
}
|
||
}
|
||
```
|
||
|
||
### @page
|
||
```css
|
||
@page {
|
||
size: A4 portrait;
|
||
margin: 2cm;
|
||
}
|
||
|
||
@page :first {
|
||
margin-top: 4cm;
|
||
}
|
||
|
||
@page :left {
|
||
margin-left: 3cm;
|
||
}
|
||
|
||
@page :right {
|
||
margin-right: 3cm;
|
||
}
|
||
|
||
/* Named page */
|
||
@page chapter {
|
||
margin: 3cm;
|
||
}
|
||
|
||
.chapter-page { page: chapter; }
|
||
```
|
||
|
||
### Page break
|
||
```css
|
||
.page-break-before { page-break-before: always; break-before: page; }
|
||
.page-break-after { page-break-after: always; break-after: page; }
|
||
.no-break { page-break-inside: avoid; break-inside: avoid; }
|
||
|
||
/* Modern syntax */
|
||
.chapter { break-before: page; }
|
||
.figure { break-inside: avoid; }
|
||
```
|
||
|
||
```html
|
||
<div class="chapter">Chapter 1</div>
|
||
<p>Content...</p>
|
||
<div class="page-break-before"></div>
|
||
<div class="chapter">Chapter 2</div>
|
||
```
|
||
|
||
### Avoid orphan / widow
|
||
```css
|
||
p {
|
||
orphans: 3; /* 페이지 끝 최소 3 line */
|
||
widows: 3; /* 페이지 시작 최소 3 line */
|
||
}
|
||
```
|
||
|
||
→ 한 줄 만 페이지 끝 / 시작 안 됨.
|
||
|
||
### Header / footer (running headers)
|
||
```css
|
||
@page {
|
||
@top-center { content: "My Document"; }
|
||
@bottom-center { content: counter(page) " / " counter(pages); }
|
||
@top-right { content: string(chapter-title); }
|
||
}
|
||
|
||
h1.chapter {
|
||
string-set: chapter-title content();
|
||
}
|
||
```
|
||
|
||
→ 매 page header 자동.
|
||
|
||
⚠️ Browser 지원 부분적. Paged.js / Prince XML 사용 가능.
|
||
|
||
### Counter (page number)
|
||
```css
|
||
@page {
|
||
@bottom-right {
|
||
content: counter(page) " / " counter(pages);
|
||
font-size: 9pt;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Image / table breaks
|
||
```css
|
||
img, table, figure {
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
/* Heading 가 단독 페이지 끝 X */
|
||
h1, h2, h3 {
|
||
break-after: avoid;
|
||
page-break-after: avoid;
|
||
}
|
||
```
|
||
|
||
### Receipts / invoices
|
||
```css
|
||
@page {
|
||
size: 80mm auto; /* thermal printer */
|
||
margin: 0;
|
||
}
|
||
|
||
@media print {
|
||
body {
|
||
font-family: monospace;
|
||
font-size: 10pt;
|
||
margin: 0;
|
||
padding: 5mm;
|
||
}
|
||
|
||
.item-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.total {
|
||
font-weight: bold;
|
||
border-top: 1px dashed black;
|
||
padding-top: 5mm;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 흑백 (cost saving)
|
||
```css
|
||
@media print {
|
||
* {
|
||
color: black !important;
|
||
background: white !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
/* 단 logo / chart 등 색 유지 */
|
||
.logo, .chart {
|
||
color: revert !important;
|
||
background: revert !important;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 색 강제 (cost OK)
|
||
```css
|
||
@media print {
|
||
.alert {
|
||
-webkit-print-color-adjust: exact;
|
||
print-color-adjust: exact; /* Modern */
|
||
background: red !important;
|
||
color: white !important;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Print button
|
||
```html
|
||
<button onclick="window.print()">Print</button>
|
||
```
|
||
|
||
```ts
|
||
window.print(); // 인쇄 dialog 열기
|
||
|
||
window.addEventListener('beforeprint', () => {
|
||
// Modify before print
|
||
});
|
||
|
||
window.addEventListener('afterprint', () => {
|
||
// 후처리
|
||
});
|
||
```
|
||
|
||
### Print preview (dev)
|
||
```
|
||
Chrome: File → Print (Cmd+P) → "Save as PDF"
|
||
또는 DevTools → 3-dot → More tools → Rendering → Emulate CSS media → print
|
||
```
|
||
|
||
→ 인쇄 dialog 열어서 preview.
|
||
|
||
### Paged.js (advanced)
|
||
```bash
|
||
yarn add pagedjs
|
||
```
|
||
|
||
```ts
|
||
import { Previewer } from 'pagedjs';
|
||
|
||
const previewer = new Previewer();
|
||
previewer.preview();
|
||
```
|
||
|
||
→ Browser 의 paged media 부족 = polyfill. CSS 표준 paged feature 더 많이 지원.
|
||
|
||
### PDF generation (server)
|
||
```ts
|
||
// Puppeteer
|
||
import puppeteer from 'puppeteer';
|
||
|
||
const browser = await puppeteer.launch();
|
||
const page = await browser.newPage();
|
||
await page.goto('https://example.com/report?id=42');
|
||
await page.emulateMediaType('print');
|
||
|
||
const pdf = await page.pdf({
|
||
format: 'A4',
|
||
printBackground: true,
|
||
margin: { top: '2cm', bottom: '2cm', left: '2cm', right: '2cm' },
|
||
displayHeaderFooter: true,
|
||
headerTemplate: '<div style="font-size:9pt; text-align:center; width:100%;">My Report</div>',
|
||
footerTemplate: '<div style="font-size:9pt; text-align:center; width:100%;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>',
|
||
});
|
||
|
||
await browser.close();
|
||
fs.writeFileSync('report.pdf', pdf);
|
||
```
|
||
|
||
→ HTML + CSS print → PDF.
|
||
|
||
### Receipt 인쇄 (browser → POS printer)
|
||
```
|
||
대부분 = browser 의 일반 print dialog → 사용자가 thermal printer 선택.
|
||
|
||
또는 ESC/POS 직접 (Bluetooth / serial / USB).
|
||
```
|
||
|
||
### CSS 실험 — bookmarks (PDF 안)
|
||
```css
|
||
h1 {
|
||
bookmark-level: 1;
|
||
bookmark-label: content();
|
||
}
|
||
h2 {
|
||
bookmark-level: 2;
|
||
}
|
||
```
|
||
|
||
→ PDF 의 navigation pane.
|
||
|
||
### Page size 표준
|
||
```
|
||
A4: 210mm × 297mm (most international)
|
||
US Letter: 8.5in × 11in
|
||
Receipt: 80mm wide
|
||
Legal: 8.5in × 14in
|
||
A5: 148mm × 210mm
|
||
```
|
||
|
||
```css
|
||
@page { size: letter; }
|
||
@page { size: 80mm 200mm; } /* custom */
|
||
```
|
||
|
||
### Orientation
|
||
```css
|
||
@page { size: landscape; }
|
||
|
||
/* Specific page */
|
||
@page wide {
|
||
size: A3 landscape;
|
||
}
|
||
|
||
.wide-table { page: wide; }
|
||
```
|
||
|
||
### Test
|
||
```ts
|
||
test('print stylesheet hides nav', () => {
|
||
document.body.classList.add('print-preview'); // 또는 @media print 검증
|
||
|
||
// Puppeteer test
|
||
await page.emulateMediaType('print');
|
||
const navVisible = await page.evaluate(() => {
|
||
return getComputedStyle(document.querySelector('nav')).display !== 'none';
|
||
});
|
||
expect(navVisible).toBe(false);
|
||
});
|
||
```
|
||
|
||
### React component
|
||
```tsx
|
||
function ReportPage() {
|
||
return (
|
||
<>
|
||
<header className="no-print">
|
||
<button onClick={() => window.print()}>Print</button>
|
||
</header>
|
||
<main>
|
||
{/* Print content */}
|
||
<h1>Report</h1>
|
||
<table>...</table>
|
||
</main>
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
|
||
```css
|
||
@media print {
|
||
.no-print { display: none; }
|
||
}
|
||
```
|
||
|
||
## 🤔 의사결정 기준
|
||
| 작업 | 추천 |
|
||
|---|---|
|
||
| 일반 web print | @media print + @page |
|
||
| PDF download | Puppeteer / Playwright server |
|
||
| Receipt | 80mm @page + monospace |
|
||
| Book / chapter | Paged.js / Prince XML |
|
||
| 단순 export | window.print() + CSS |
|
||
| Server batch PDF | Puppeteer + queue |
|
||
|
||
## ❌ 안티패턴
|
||
- **Print stylesheet 무**: 사용자 인쇄 시 깨짐.
|
||
- **`!important` 없는 background hide**: 옛 browser 가 무시.
|
||
- **`page-break-inside: auto` 큰 image**: 잘림.
|
||
- **Widows / orphans 무시**: 작은 element 1줄 단독.
|
||
- **Color 인쇄 강제 (사용자 흑백 원함)**: print-color-adjust: economy.
|
||
- **JS 가 print 시 작동 가정**: 일부 browser 에서 X.
|
||
- **Print preview 없이 prod**: 디자인 깨짐 모름.
|
||
|
||
## 🤖 LLM 활용 힌트
|
||
- @media print 항상 + .no-print class.
|
||
- Server PDF = Puppeteer.
|
||
- 책 / 복잡 = Paged.js.
|
||
- Print preview 가 진짜 test.
|
||
|
||
## 🔗 관련 문서
|
||
- [[Frontend_CSS_Modern_Features]]
|
||
- [[Frontend_A11y_Testing]]
|
||
- [[Frontend_Image_Optimization]]
|