POSA_LEAKSMS/php/assets/report-print.css
cw2007 06f96a8d09 feat(report): 월간 보고서 PDF 저장 전용 A4 문서 뷰
다크 대시보드를 그대로 인쇄하던 '단순 프린트지'를 화이트 A4 전문 보고서로 대체.
?format=print 로 브라우저 print-to-PDF 하면 표지·요약·차트·서명을 갖춘 정식 문서가
나온다. 설계 3안(경영진 브리프/보안증적/비주얼 애널리틱스) 병렬 → 종합 → 구현.
실제 브라우저 렌더로 6개 섹션 전부 검증.

구성:
- 표지 마스트헤드: 대형 대상월 + 관제 판정 씰(SVG 원형 도장 — 데이터 자동 판정
  安定/注意/警報)이 시그니처. 러닝 헤더/푸터(대외비·문서번호·시스템).
- 01 경영 요약: 헤드라인 KPI 4(SMS신뢰성 게이지·총경보·누수·측정) + 자동 총평 내러티브.
- 02 월간 지표: 경보·이벤트 / 온습도 측정 2열 정의형.
- 03 추이(인라인 SVG): 일별 평균 온·습도(안전/고온/저온 밴드·임계선), 일별 경보 스택
  막대(온습도=네이비 / 누수=적색 사선 해치 — 흑백 인쇄 구분).
- 04 관제 지점별 상세 카드, 05 누수 사고 로그(지속시간·복구), 06 기준·주석 및 서명란.

구현:
- 집계를 monthly_report_data.php(공유 include)로 추출 — HTML 조회·CSV·PDF 뷰가 단일
  소스 사용(회귀 0: HTML 표 3행·요약 11카드·CSV 3행 렌더 확인). 일별 추이·누수 사고·
  판정 파생은 인쇄 뷰 전용으로 추가.
- monthly_report_print.php(A4 뷰) + report_charts.php(인라인 SVG 차트) + assets/report-print.css
  (@page A4·여백·page-break·running header/footer·print-color-adjust). app.css 다크 토큰과
  격리(.report-paper 스코프, report-print.css 단독 링크).
- 폐쇄망 준수: 외부 CDN/JS/폰트/PDF 라이브러리 0, 시스템 폰트, 인라인 SVG.
- 보안: 신규 include 3파일 직접접근 가드 + .htaccess FilesMatch 차단(monthly_report.php
  본체는 진입 페이지라 제외). sensor_label 라벨·htmlspecialchars 이스케이프.
2026-07-11 16:56:02 +09:00

285 lines
16 KiB
CSS

/* =============================================================================
* report-print.css — 월간 운영 보고서(PDF 저장 전용) A4 화이트 문서 스타일
*
* monthly_report_print.php 만 링크한다(app.css 미링크 — 다크 셸 토큰과 격리).
* 폐쇄망: 자기호스팅 로컬 파일. 외부 폰트/CDN 0 — 시스템 폰트만.
* 화면(@media screen)에서는 회색 캔버스 위에 뜬 흰 A4 종이로 미리보기(WYSIWYG),
* 인쇄(@media print)에서는 캔버스·툴바가 벗겨지고 종이가 곧 PDF 가 된다.
*
* 대상 브라우저: Chrome / Edge(Chromium) — 고정 러닝 헤더/푸터·색 보존 안정.
* FLAG: Chromium print-to-PDF 는 @page 마진박스 counter(page) 미지원 + 폐쇄망
* JS 폴리필 금지 → 라이브 페이지번호 미탑재. 고정 푸터에 문서번호·생성일시·대외비를
* 매 장 반복해 추적성을 확보하고, 화면 노트로 인쇄 대화상자의 머리글/바닥글 옵션을 안내.
* ============================================================================= */
.report-paper {
/* 라이트 고정 팔레트 — app.css @media print 토큰과 값 정합(흑백/컬러 안전) */
--paper-ink: #1a1f2b;
--paper-sub: #5b6472;
--paper-navy: #14315e;
--paper-gold: #9a7b28;
--hair: #d9dee6;
--hair-strong: #c2cad6;
--ok: #0a6b39;
--warn: #8a5a00;
--alert: #b11f1f;
--band-safe: #e7f0ea;
--band-hot: #f6e3dc;
--band-cold: #dfeef2;
--paper-bg-soft: #f4f6f9;
}
/* 색 보존 강제(네이비 밴드·골드 룰·판정 씰·차트 획 유실 방지) */
.report-paper,
.report-paper * {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* ── @page: A4 세로 + 여백 ─────────────────────────────────────────────────── */
@page {
size: A4 portrait;
margin: 18mm 16mm 20mm;
}
@page :first {
margin-top: 22mm; /* 표지 상단 여유 */
}
/* ── 종이 본체 ─────────────────────────────────────────────────────────────── */
.report-paper {
font-family: "Malgun Gothic", "맑은 고딕", "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;
color: var(--paper-ink);
background: #fff;
line-height: 1.6;
font-size: 10.5pt;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
}
.report-paper h1, .report-paper h2, .report-paper h3 { margin: 0; font-weight: 800; }
.report-paper p { margin: 0; }
.report-paper .num { font-variant-numeric: tabular-nums; }
/* ── 러닝 헤더/푸터(position:fixed — Chromium 이 매 장 반복 렌더) ─────────────── */
.run-header, .run-footer {
position: fixed;
left: 0; right: 0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 8pt;
color: var(--paper-sub);
letter-spacing: .02em;
}
.run-header { top: 0; border-bottom: 0.6pt solid var(--hair); padding-bottom: 3pt; }
.run-footer { bottom: 0; border-top: 0.6pt solid var(--hair); padding-top: 3pt; }
.run-header .cls, .run-footer .cls { color: var(--alert); font-weight: 700; }
.run-header b { color: var(--paper-navy); font-weight: 800; }
/* ── 표지 마스트헤드 ───────────────────────────────────────────────────────── */
.cover { position: relative; padding-top: 4mm; }
.cover-eyebrow {
font-size: 9.5pt; font-weight: 700; letter-spacing: .35em;
color: var(--paper-navy); margin-bottom: 6mm;
}
.cover-eyebrow::before {
content: ""; display: inline-block; width: 26pt; height: 3pt; vertical-align: middle;
background: var(--paper-gold); margin-right: 8pt; border-radius: 2pt;
}
.cover-title {
font-size: 27pt; line-height: 1.15; color: var(--paper-navy);
letter-spacing: -.01em; margin-bottom: 3mm; max-width: 70%;
}
.cover-sub { font-size: 11pt; color: var(--paper-sub); margin-bottom: 10mm; }
.cover-month {
font-size: 58pt; font-weight: 800; color: var(--paper-navy);
line-height: 1; letter-spacing: -.02em;
}
.cover-period { font-size: 10.5pt; color: var(--paper-sub); margin-top: 2mm; }
.cover-seal { position: absolute; top: 0; right: 0; text-align: center; }
.cover-seal .seal-cap { font-size: 8pt; color: var(--paper-sub); margin-top: 2pt; }
/* 관제 대상 3개소 스트립 */
.cover-scope {
display: flex; gap: 6mm; margin-top: 10mm; padding-top: 6mm;
border-top: 1pt solid var(--hair-strong);
}
.scope-item { flex: 1; }
.scope-item .k { font-size: 8.5pt; color: var(--paper-sub); letter-spacing: .1em; }
.scope-item .v { font-size: 12pt; font-weight: 800; color: var(--paper-navy); }
.scope-item .t { font-size: 8.5pt; color: var(--paper-sub); }
/* 문서 통제 블록 */
.doc-control {
margin-top: 12mm; border: 1pt solid var(--hair-strong); border-radius: 2pt;
display: grid; grid-template-columns: repeat(3, 1fr); overflow: hidden;
}
.doc-control > div { padding: 3.2mm 4mm; border-right: 0.6pt solid var(--hair); border-bottom: 0.6pt solid var(--hair); }
.doc-control > div:nth-child(3n) { border-right: none; }
.doc-control .dk { font-size: 8pt; color: var(--paper-sub); letter-spacing: .08em; margin-bottom: 1pt; }
.doc-control .dv { font-size: 10pt; font-weight: 700; color: var(--paper-ink); }
.doc-control .dv.cls { color: var(--alert); }
/* ── 섹션 제목 ─────────────────────────────────────────────────────────────── */
.section { margin-top: 8mm; }
.section-title {
font-size: 13pt; color: var(--paper-navy); padding-bottom: 2mm;
border-bottom: 1.6pt solid var(--paper-navy); margin-bottom: 4mm;
display: flex; align-items: baseline; gap: 8pt;
break-after: avoid; page-break-after: avoid;
}
.section-title .en { font-size: 8.5pt; font-weight: 600; color: var(--paper-sub); letter-spacing: .12em; }
.section-title .no {
font-size: 9pt; font-weight: 800; color: #fff; background: var(--paper-navy);
border-radius: 2pt; padding: 1pt 6pt; letter-spacing: .05em;
}
/* ── 경영 요약 KPI ─────────────────────────────────────────────────────────── */
.kpi-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4mm; }
.kpi-card {
border: 1pt solid var(--hair-strong); border-radius: 2pt; padding: 3.5mm 4mm;
break-inside: avoid; page-break-inside: avoid; position: relative;
}
.kpi-card::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3pt; background: var(--paper-navy); }
.kpi-card.is-alert::before { background: var(--alert); }
.kpi-card.is-warn::before { background: var(--warn); }
.kpi-card.is-ok::before { background: var(--ok); }
.kpi-card .kpi-k { font-size: 8.5pt; color: var(--paper-sub); letter-spacing: .04em; }
.kpi-card .kpi-v { font-size: 21pt; font-weight: 800; color: var(--paper-navy); line-height: 1.1; }
.kpi-card.is-alert .kpi-v { color: var(--alert); }
.kpi-card .kpi-s { font-size: 8.5pt; color: var(--paper-sub); }
.kpi-gauge-wrap { text-align: center; }
.exec-summary {
margin-top: 4mm; border: 1pt solid var(--hair-strong); border-left: 3pt solid var(--paper-navy);
border-radius: 2pt; padding: 3.5mm 4.5mm; background: var(--paper-bg-soft);
break-inside: avoid; page-break-inside: avoid;
}
.exec-summary .verdict-line { font-size: 11pt; font-weight: 800; color: var(--paper-navy); }
.exec-summary .verdict-line.is-alert { color: var(--alert); }
.exec-summary .verdict-line.is-warn { color: var(--warn); }
.exec-summary .verdict-line.is-ok { color: var(--ok); }
.exec-summary p { font-size: 10pt; color: var(--paper-ink); margin-top: 1.5mm; }
/* ── 정의형 지표 표(경보·이벤트 / 온습도) ──────────────────────────────────── */
.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6mm; }
.metric-block { break-inside: avoid; page-break-inside: avoid; }
.metric-block h3 { font-size: 10pt; color: var(--paper-navy); margin-bottom: 2mm; }
table.def { width: 100%; border-collapse: collapse; font-size: 9.5pt; }
table.def th, table.def td { padding: 2mm 3mm; border-bottom: 0.6pt solid var(--hair); text-align: left; }
table.def th { color: var(--paper-sub); font-weight: 600; width: 55%; }
table.def td { text-align: right; font-weight: 700; color: var(--paper-ink); font-variant-numeric: tabular-nums; }
table.def td.is-alert { color: var(--alert); }
/* ── 일반 데이터 표(지점별 등) ─────────────────────────────────────────────── */
table.grid { width: 100%; border-collapse: collapse; font-size: 9pt; }
table.grid thead { display: table-header-group; } /* 페이지 넘김 시 머리행 반복 */
table.grid th, table.grid td { padding: 2mm 2.5mm; border: 0.6pt solid var(--hair); text-align: center; }
table.grid th { background: var(--paper-navy); color: #fff; font-weight: 700; font-size: 8.5pt; }
table.grid td.l { text-align: left; }
table.grid tbody tr:nth-child(even) td { background: var(--paper-bg-soft); }
table.grid td.is-alert { color: var(--alert); font-weight: 800; }
/* ── 지점별 카드 ───────────────────────────────────────────────────────────── */
.loc-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4mm; }
.loc-card {
border: 1pt solid var(--hair-strong); border-radius: 2pt; padding: 3.5mm 4mm;
break-inside: avoid; page-break-inside: avoid;
}
.loc-card .lc-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2mm; padding-bottom: 2mm; border-bottom: 0.8pt solid var(--hair); }
.loc-card .lc-name { font-size: 11.5pt; font-weight: 800; color: var(--paper-navy); }
.loc-card .lc-loc { font-size: 8.5pt; color: var(--paper-sub); }
.pill {
font-size: 8pt; font-weight: 800; padding: 1pt 6pt; border-radius: 8pt;
border: 0.8pt solid currentColor;
}
.pill.is-ok { color: var(--ok); }
.pill.is-warn { color: var(--warn); }
.pill.is-alert { color: var(--alert); }
.loc-card dl { display: grid; grid-template-columns: auto 1fr; gap: 1mm 3mm; margin: 0; font-size: 9pt; }
.loc-card dt { color: var(--paper-sub); }
.loc-card dd { margin: 0; text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
.loc-card dd.is-alert { color: var(--alert); }
/* ── 차트 패널 ─────────────────────────────────────────────────────────────── */
.chart-panel {
border: 1pt solid var(--hair-strong); border-radius: 2pt; padding: 4mm;
margin-top: 3mm; break-inside: avoid; page-break-inside: avoid;
}
.chart-panel h3 { font-size: 10pt; color: var(--paper-navy); margin-bottom: 1mm; }
.chart-panel .chart-cap { font-size: 8.5pt; color: var(--paper-sub); margin-bottom: 2mm; }
.chart-empty { padding: 8mm; text-align: center; color: var(--paper-sub); font-size: 9.5pt; border: 0.8pt dashed var(--hair-strong); border-radius: 2pt; }
.chart-legend-row { display: flex; flex-wrap: wrap; gap: 4mm; margin-top: 2mm; font-size: 8.5pt; color: var(--paper-sub); }
.chart-legend-row span { display: inline-flex; align-items: center; gap: 4pt; }
.lg-sw { width: 11pt; height: 8pt; border-radius: 1pt; display: inline-block; }
.lg-sw.hatch { background: repeating-linear-gradient(45deg, var(--alert) 0 2pt, #fff 2pt 3.5pt); border: 0.5pt solid var(--alert); }
.lg-line { width: 16pt; height: 3pt; border-radius: 2pt; display: inline-block; }
.lg-line.dash { background: repeating-linear-gradient(90deg, currentColor 0 4pt, transparent 4pt 7pt) !important; }
.lg-band { width: 12pt; height: 9pt; border-radius: 1pt; display: inline-block; border: 0.5pt solid var(--hair-strong); }
/* ── 누수 사고 로그 ────────────────────────────────────────────────────────── */
.incident-none {
border: 1pt solid var(--ok); border-radius: 2pt; padding: 4mm 4.5mm;
color: var(--ok); font-weight: 700; font-size: 10pt; background: var(--band-safe);
}
/* ── 기준·주석 + 서명란 ────────────────────────────────────────────────────── */
.basis-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6mm; }
.basis-block { break-inside: avoid; page-break-inside: avoid; }
.basis-block h3 { font-size: 9.5pt; color: var(--paper-navy); margin-bottom: 2mm; }
.basis-block ul { margin: 0; padding-left: 4.5mm; font-size: 9pt; color: var(--paper-ink); }
.basis-block li { margin-bottom: 1mm; }
.basis-block li b { color: var(--paper-navy); }
.sign-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6mm; margin-top: 8mm; break-inside: avoid; page-break-inside: avoid; }
.sign-box { border: 1pt solid var(--hair-strong); border-radius: 2pt; }
.sign-box .sk { font-size: 8.5pt; color: var(--paper-sub); padding: 2mm 3mm; border-bottom: 0.6pt solid var(--hair); background: var(--paper-bg-soft); }
.sign-box .sv { height: 16mm; }
.sign-box .sn { font-size: 8pt; color: var(--paper-sub); padding: 1.5mm 3mm; border-top: 0.6pt solid var(--hair); text-align: right; }
/* ── 페이지 브레이크 ───────────────────────────────────────────────────────── */
.page-break { break-before: page; page-break-before: always; }
/* ── 화면 미리보기(회색 캔버스 위 흰 종이) ─────────────────────────────────── */
@media screen {
body { margin: 0; background: #e9eaee; }
.report-canvas { padding: 24px 12px 64px; }
.report-paper {
width: 210mm; max-width: 100%; min-height: 297mm; margin: 0 auto;
padding: 22mm 16mm 20mm; background: #fff; box-sizing: border-box;
box-shadow: 0 6px 28px rgba(20, 30, 60, .18);
}
/* 화면에서는 고정 헤더/푸터를 종이 안쪽에 흐름 배치(겹침 방지) */
.run-header, .run-footer { position: static; margin: 0 auto; width: 210mm; max-width: 100%; }
.run-header { margin-bottom: 6mm; }
.run-footer { margin-top: 6mm; }
.print-note {
width: 210mm; max-width: 100%; margin: 0 auto 16px; padding: 10px 14px;
background: #fff8e6; border: 1px solid #e6c766; border-radius: 6px;
font-size: 12px; color: #6b5410; box-sizing: border-box;
font-family: "Malgun Gothic", "맑은 고딕", "Apple SD Gothic Neo", sans-serif;
}
.print-note b { color: #8a5a00; }
.print-note a { color: #14315e; }
.screen-toolbar {
width: 210mm; max-width: 100%; margin: 0 auto 12px; display: flex; gap: 8px; flex-wrap: wrap;
box-sizing: border-box;
}
.screen-toolbar a, .screen-toolbar button {
font: inherit; font-size: 13px; font-weight: 700; padding: 8px 14px; border-radius: 6px;
border: 1px solid #14315e; background: #14315e; color: #fff; cursor: pointer; text-decoration: none;
font-family: "Malgun Gothic", "맑은 고딕", "Apple SD Gothic Neo", sans-serif;
}
.screen-toolbar a.ghost { background: #fff; color: #14315e; }
}
/* ── 인쇄(종이가 곧 PDF) ───────────────────────────────────────────────────── */
@media print {
html, body { margin: 0; background: #fff; }
.report-canvas { padding: 0; background: #fff; }
.report-paper {
width: auto; max-width: none; min-height: 0; margin: 0; padding: 0;
background: #fff; box-shadow: none;
}
.print-note, .screen-toolbar, .screen-only { display: none !important; }
/* 고정 헤더/푸터와 본문 겹침 방지 여백 */
.report-body { padding-top: 6mm; padding-bottom: 6mm; }
}