다크모드
관리비 고지서 대량 인쇄 파이프라인 (BE 참고)
독자: BE 개발자/AI. 관리비 고지서(수백~수천 세대/월)의 서버사이드 nunjucks 렌더 + Chromium PDF 배치 계약. 방향 검토 정본:
docs/decisions/REVIEW-INVOICE-MASS-PRINT-2026-07-10.md(nunjucks 채택 판정 + 1,000세대 38s 실측). 레퍼런스 구현(프로토):templates/transaction/invoice-pmf.njk+scripts/invoices-pdf.mjs(npm run invoices:pdf).
1. 구조 — 계약은 JSON, 표현은 njk
InvoiceVM JSON (계약 — §2)
├─ 화면: Vue InvoiceDynamicOrg ← useInvoice.js가 FE에서 조립 (프로토 as-built)
└─ 인쇄: invoice-pmf.njk ← BE가 실 DB에서 같은 JSON을 채워 SSR (본 문서)
└─ Chromium page.pdf() — 100세대/chunk 이어붙여 1회 변환 (§4)- 템플릿은 계산·집계 금지 — 표시 포맷팅(comma)만. 모든 금액·합계는 VM에 완성값으로 담아 보낸다(청구액 재계산 금지 — useInvoice 주석의 "payable 권위" 원칙과 동일).
- 디자인 변경의 정본 = print.leysys.net 목업. 목업 → njk → (필요시) Vue 순 전파. njk와 Vue는 서로 참조하지 않는다.
2. 데이터 계약 — InvoiceVM + print 보충
정본 픽스처: templates/transaction/invoice-sample.json. 골격(키는 useInvoice.js 산출과 동일 — 한글 키 유지):
| 섹션 | 내용 | 소스 |
|---|---|---|
header | invoiceCode·periodLabel·periodRange·assessmentDate·dueDate·dueDateLabel·manager{brand,name,shortName,addr,tel,bizNo}·recipient | 청구 헤더 |
summary | 세전부과원금·부가세·미납원금·미납연체금·합계·선수금·청구액 (7행 — 완성값) | useInvoice 7행 요약과 동일 |
byTaxGroup | 과세합계·면세합계·영세합계·계산서비대상·부가세 | 의뢰서(Min) |
lineItems[] | {label, amount, taxMark} × 26 표준항목 (label 한글 최대 7자 권장, *=면세 **=계산서비대상) | 영수증(Max) — invoiceDisplayPreset 26항목 |
metering | usage[]{item,currentReading,prevReading,currentUsage,prevUsage} · comparison[]{item,current,prev} (최대 4) | 검침 |
payment[] | {bank, account, holder} (최대 2) | 납부계좌 |
print | 인쇄 전용 보충: gauge{usage,average,ratioLabel,direction}?·notices[](≤5)·personalNotices[](≤2)·discounts[]{label,amount}·discountTotal·afterDue{carryLate,currentLate,total}?·autopay·bannerImage?·qrDataUri? | BE 제공. ?는 null 시 섹션 자동 생략 |
볼륨 규칙(고정 A4 1장 전제): 공지 ≤5행 · 1:1 공지 ≤2행 · 비교 ≤4항목 · 사용량 ≤7행 · 계좌 ≤2 · lineItems=26 고정. 초과분은 BE에서 절삭/선별(템플릿은 자르지 않는다).
3. 렌더러 계약 (nunjucks 환경)
js
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(tplDir), { autoescape: true });
env.addFilter("comma", (n) => Number(n || 0).toLocaleString("ko-KR")); // 필수 필터
env.render("invoice-pmf.njk", invoiceVm);autoescape: true필수 — 공지·상호 등 사용자 입력의 XSS/마크업 깨짐 방지(vitest로 고정).- 한글 키는 템플릿에서 bracket 접근(
summary['청구액']) — nunjucks 식별자 제약.
4. PDF 배치 전략 (1,000세대 38s 실측 — 검토 §4·§8)
- chunk: 100세대를
.invoice-page(297mm 고정·page-break) div로 이어붙여 HTML 1개 →page.pdf()1회. 장당 개별 호출 금지(수십 배 느림). - 브라우저 재사용: launch 1회,
page.setContent()반복. 최초 chunk에서document.fonts.ready대기. - 병렬: chunk를 워커 2~4개로 — 수천 세대 분 단위.
- 산출 분리: 세대별 개별 PDF가 필요하면 chunk PDF를 pdf-lib로 페이지 분할(재렌더 금지).
- 실패 격리: 세대 단위 try/render — 오류 세대는 목록으로 리포트하고 배치는 계속(TraceId 패턴).
5. CSS 번들 (standalone)
src/assets/styles/print/(콘솔 정본 13파일)을 main.css의 @layer 순서 그대로 이어붙이고 + templates/transaction/print-standalone-shim.css(제네릭 유틸 + pmf 카드 flex 배분)를 뒤에 붙인다. 함정 2건 — 검토 문서 §8 필독: ① CSS 파일 선두 BOM 제거 필수(셀렉터 매칭 전멸), ② print-card-main-unscoped/card-footer-scoped는 shim이 배분 정의(없으면 절취 푸터가 A4 밖으로).
6. 테스트
templates/transaction/invoice-njk.spec.js— 계약 정합(7행 요약 그대로 표기·26항목 전수·null 내성·autoescape). VM 스키마를 바꾸면 여기부터 깨진다 — 계약 위반 조기 검출용이니 삭제 금지.