POSA_LEAKSMS/review/ui_mockup_v2.html
cw2007 90b93f818c chore(deploy+docs): 노드 번들 생성기·하드닝 갱신 + 설계문서
- tools/make_deploy_bundles.py: PMS7003·Zero2W 디스플레이 노드 번들 생성 반영(폐쇄망 동봉)
- tools/harden_pi.sh: 하드닝 스크립트 갱신
- docs/superpowers: 노드 센서 통합·스모크 파일럿·대시보드 다크 설계/계획 문서
- docs/claude-config: IoT 납품·검증 스킬/룰 참조본
- review/: 프로젝트 리뷰·UI v2 목업
2026-07-19 10:48:09 +09:00

912 lines
46 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>누수·온습도 관제: UI 개선 목업 v2</title>
<style>
/* ============================================================
누수·온습도 통합 관제 — UI 개선 목업
원칙: Apple fluid interface — 즉각 반응(:active), 반투명 재질,
광학 타이포(큰 글자 음수 트래킹), spring 유사 easing,
prefers-reduced-motion 대응. 폐쇄망 전제: 외부 리소스 0.
============================================================ */
:root {
--bg: #0d1024;
--bg-2: #12162e;
--surface: rgba(255,255,255,.045);
--surface-2: rgba(255,255,255,.08);
--line: rgba(255,255,255,.09);
--ink: #eef0fa;
--ink-2: #9aa1c2;
--ink-3: #5d648a;
--ok: #34d399;
--warn: #fbbf24;
--err: #fb7185;
--leak: #60a5fa;
--temp: #f97362;
--hum: #38bdf8;
--accent: #818cf8;
--r-lg: 18px;
--r-md: 12px;
--r-sm: 8px;
--spring: cubic-bezier(.32,.72,.28,1); /* 감쇠 스프링 근사 */
}
* { margin:0; padding:0; box-sizing:border-box; }
html { -webkit-text-size-adjust:100%; }
body {
font: 15px/1.55 system-ui, -apple-system, "Segoe UI", "Malgun Gothic", sans-serif;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(129,140,248,.14), transparent 60%),
radial-gradient(900px 500px at -10% 30%, rgba(56,189,248,.08), transparent 55%),
var(--bg);
color: var(--ink);
min-height: 100vh;
padding-bottom: 60px;
}
a { color: inherit; text-decoration: none; }
/* ── 상단 글래스 툴바: 콘텐츠가 아래로 스크롤되는 반투명 재질 ── */
.topbar {
position: sticky; top: 0; z-index: 50;
display: flex; align-items: center; gap: 16px;
padding: 14px 28px;
background: rgba(13,16,36,.72);
backdrop-filter: blur(20px) saturate(160%);
-webkit-backdrop-filter: blur(20px) saturate(160%);
border-bottom: 1px solid var(--line);
}
.brand { display:flex; align-items:center; gap:10px; font-weight:700; letter-spacing:-.01em; }
.brand .dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--ok); box-shadow: 0 0 12px var(--ok);
animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity:.45; } }
.brand small { color: var(--ink-3); font-weight: 400; margin-left: 4px; }
.topbar .spacer { flex:1; }
.top-link {
font-size: .8125rem; color: var(--ink-2);
padding: 7px 12px; border-radius: var(--r-sm);
transition: background .18s var(--spring), color .18s;
}
.top-link:hover { background: var(--surface-2); color: var(--ink); }
.top-link:active { transform: scale(.96); }
.refresh-pill {
display:flex; align-items:center; gap:8px;
font-size:.75rem; color:var(--ink-2);
border:1px solid var(--line); border-radius:999px;
padding:5px 6px 5px 12px; background:var(--surface);
}
.refresh-pill button {
all:unset; cursor:pointer; font-size:.75rem; color:var(--ink);
background:var(--surface-2); border-radius:999px; padding:3px 10px;
transition: transform .15s var(--spring), background .18s;
}
.refresh-pill button:active { transform: scale(.93); }
.refresh-ring { width:16px; height:16px; }
.refresh-ring circle { fill:none; stroke:var(--accent); stroke-width:2.5;
stroke-dasharray: 38; stroke-linecap: round;
transform: rotate(-90deg); transform-origin: center;
animation: countdown 30s linear infinite; }
@keyframes countdown { from{stroke-dashoffset:0} to{stroke-dashoffset:38} }
.container { max-width: 1240px; margin: 0 auto; padding: 24px 28px; }
/* ── 상태 히어로: 시스템 전체 판정 1문장 + 큰 광학 타이포 ── */
.hero {
display:flex; align-items:center; justify-content:space-between; gap:24px;
padding: 26px 28px;
border-radius: var(--r-lg);
border: 1px solid rgba(52,211,153,.25);
background: linear-gradient(120deg, rgba(52,211,153,.12), rgba(52,211,153,.03) 55%, transparent);
margin-bottom: 18px;
}
.hero.err {
border-color: rgba(251,113,133,.35);
background: linear-gradient(120deg, rgba(251,113,133,.16), rgba(251,113,133,.04) 55%, transparent);
}
.hero h1 {
font-size: clamp(1.4rem, 2.6vw, 1.9rem);
letter-spacing: -.02em; line-height: 1.15; font-weight: 700;
}
.hero .sub { color: var(--ink-2); margin-top: 6px; font-size:.875rem; }
.hero .clock { text-align:right; color:var(--ink-3); font-size:.8125rem; font-variant-numeric: tabular-nums; }
.hero .clock b { display:block; font-size:1.05rem; color:var(--ink-2); font-weight:600; }
/* ── 알림 스트립 ── */
.strip {
display:flex; align-items:center; gap:12px;
padding: 12px 18px; border-radius: var(--r-md);
margin-bottom: 10px; font-size:.875rem;
border:1px solid rgba(251,113,133,.3);
background: rgba(251,113,133,.08);
transition: transform .18s var(--spring), background .18s;
}
.strip:hover { transform: translateX(3px); background: rgba(251,113,133,.13); }
.strip:active { transform: scale(.99); }
.strip .ico { font-size:1rem; }
.strip strong { color:#fecdd3; }
.strip span { color:var(--ink-2); flex:1; }
.strip em { font-style:normal; color:var(--ink-2); font-size:.8125rem; white-space:nowrap; }
.strip.warn { border-color: rgba(251,191,36,.3); background: rgba(251,191,36,.07); }
.strip.warn:hover { background: rgba(251,191,36,.12); }
.strip.warn strong { color:#fde68a; }
/* ── KPI 스트립: 큰 숫자 + 미니 스파크, 딥링크 카드 ── */
.kpis {
display:grid; grid-template-columns: repeat(6, 1fr); gap:12px;
margin: 18px 0;
}
.kpi {
display:block; padding: 16px 18px 14px;
background: var(--surface); border:1px solid var(--line);
border-radius: var(--r-md);
transition: transform .2s var(--spring), border-color .2s, background .2s;
}
.kpi:hover { transform: translateY(-3px); background: var(--surface-2); border-color: rgba(255,255,255,.18); }
.kpi:active { transform: translateY(-1px) scale(.98); }
.kpi .lbl { font-size:.75rem; color:var(--ink-2); letter-spacing:.01em; }
.kpi .val {
font-size: 1.9rem; font-weight: 700; letter-spacing: -.03em;
line-height: 1.1; margin-top: 4px; font-variant-numeric: tabular-nums;
}
.kpi .val small { font-size:.9rem; font-weight:500; color:var(--ink-2); letter-spacing:0; margin-left:2px; }
.kpi .sub { font-size:.71rem; color:var(--ink-3); margin-top:3px; }
.kpi.ok .val { color: var(--ok); }
.kpi.warn .val { color: var(--warn); }
.kpi.err .val { color: var(--err); }
.kpi.neutral .val { color: var(--ink); }
/* ── 세그먼트 탭바 (iOS segmented control 형태) ── */
.tabs {
display:flex; gap:4px; padding:4px;
background: var(--surface); border:1px solid var(--line);
border-radius: 999px; width: fit-content; margin-bottom: 26px;
}
.tab {
padding: 8px 20px; border-radius:999px; font-size:.875rem; color:var(--ink-2);
position:relative; transition: color .18s, background .22s var(--spring), transform .15s var(--spring);
}
.tab:hover { color: var(--ink); }
.tab:active { transform: scale(.96); }
.tab.active {
color:#fff; background: linear-gradient(135deg, #6366f1, #8b5cf6);
box-shadow: 0 4px 14px rgba(99,102,241,.35);
}
.tab .badge {
display:inline-flex; min-width:17px; height:17px; align-items:center; justify-content:center;
border-radius:999px; background:var(--err); color:#fff;
font-size:.66rem; font-weight:700; padding:0 5px; margin-left:6px; vertical-align:1px;
}
/* ── 섹션 헤더 ── */
h2.sec {
font-size: 1.05rem; font-weight: 700; letter-spacing: -.015em;
margin: 26px 0 14px; display:flex; align-items:baseline; gap:8px;
}
h2.sec small { font-weight:400; color:var(--ink-3); font-size:.8125rem; }
/* ── 센서 카드 ── */
.grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(272px,1fr)); gap:14px; }
.card {
background: var(--surface); border:1px solid var(--line);
border-radius: var(--r-lg); padding: 18px;
position:relative; overflow:hidden;
transition: transform .22s var(--spring), border-color .22s, box-shadow .22s;
}
.card:hover { transform: translateY(-3px); box-shadow: 0 12px 32px rgba(0,0,0,.35); border-color: rgba(255,255,255,.16); }
.card::before {
content:""; position:absolute; inset:0 0 auto 0; height:3px;
background: var(--ok); opacity:.85;
}
.card.alert::before { background: var(--err); }
.card.leak::before { background: var(--leak); }
.card.offline::before { background: var(--ink-3); }
.card.alert { border-color: rgba(251,113,133,.35); background: rgba(251,113,133,.05); }
.card.leak { border-color: rgba(96,165,250,.35); background: rgba(96,165,250,.05); }
.card.offline { opacity:.72; }
.card.leak.pulse { animation: leakGlow 1.8s ease-in-out infinite; }
@keyframes leakGlow { 50% { box-shadow: 0 0 26px rgba(96,165,250,.28); } }
.c-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:2px; }
.c-head strong { font-size:1rem; letter-spacing:-.01em; }
.chip-type {
font-size:.66rem; padding:3px 9px; border-radius:999px; font-weight:600;
background: rgba(56,189,248,.14); color: var(--hum);
}
.chip-type.leak { background: rgba(96,165,250,.14); color: var(--leak); }
.c-loc { font-size:.75rem; color:var(--ink-3); margin-bottom:12px; }
.status-line { display:flex; align-items:center; gap:7px; font-size:.8125rem; font-weight:600; margin-bottom:12px; }
.status-dot { width:8px; height:8px; border-radius:50%; background:var(--ok); box-shadow:0 0 8px var(--ok); }
.status-line.ok { color: var(--ok); }
.status-line.err { color: var(--err); } .status-line.err .status-dot { background:var(--err); box-shadow:0 0 8px var(--err); animation:pulse 1.2s infinite; }
.status-line.leak { color: var(--leak); } .status-line.leak .status-dot { background:var(--leak); box-shadow:0 0 8px var(--leak); animation:pulse 1.2s infinite; }
.status-line.off { color: var(--ink-3); } .status-line.off .status-dot { background:var(--ink-3); box-shadow:none; }
.vals { display:flex; gap:18px; margin-bottom:8px; }
.v { flex:1; }
.v .n {
font-size:1.65rem; font-weight:700; letter-spacing:-.03em;
font-variant-numeric: tabular-nums; line-height:1.1;
}
.v .n small { font-size:.85rem; color:var(--ink-2); font-weight:500; letter-spacing:0; }
.v .l { font-size:.7rem; color:var(--ink-3); margin-top:2px; }
.v.temp .n { color: var(--temp); } .v.hum .n { color: var(--hum); }
.v .n.hot { text-shadow: 0 0 14px rgba(249,115,98,.5); }
.spark { width:100%; height:34px; margin: 2px 0 8px; }
.spark polyline { fill:none; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
.spark .t { stroke: var(--temp); opacity:.85; }
.spark .h { stroke: var(--hum); opacity:.85; }
.fchips { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
.fchip {
font-size:.69rem; padding:3px 9px; border-radius:999px;
background: var(--surface-2); color: var(--ink-2); border:1px solid var(--line);
}
.fchip.near { background: rgba(251,191,36,.12); color:#fde68a; border-color:rgba(251,191,36,.25); }
.fchip.over { background: rgba(251,113,133,.12); color:#fecdd3; border-color:rgba(251,113,133,.3); }
.c-foot { font-size:.71rem; color:var(--ink-3); margin-top:10px; display:flex; justify-content:space-between; }
.c-foot .stale { color: var(--err); font-weight:600; }
/* 누수카드 물결 배경 */
.wave {
position:absolute; left:0; right:0; bottom:0; height:46px; opacity:.16; pointer-events:none;
}
/* ── 미니 패널 ── */
.panels { display:grid; grid-template-columns: repeat(auto-fit,minmax(300px,1fr)); gap:14px; }
.panel {
background: var(--surface); border:1px solid var(--line);
border-radius: var(--r-lg); padding: 18px 20px;
}
.panel h3 { font-size:.9375rem; letter-spacing:-.01em; margin-bottom:12px; }
.rows div { display:flex; justify-content:space-between; padding:7px 0; border-bottom:1px dashed var(--line); font-size:.8438rem; }
.rows div:last-child { border-bottom:none; }
.rows dt { color:var(--ink-2); }
.rows dd { font-weight:600; font-variant-numeric: tabular-nums; }
.rows .err-c { color: var(--err); } .rows .ok-c { color: var(--ok); }
.panel .note { font-size:.71rem; color:var(--ink-3); margin-top:10px; }
.gauge { display:flex; align-items:center; gap:14px; margin-bottom:8px; }
.gauge svg { width:64px; height:64px; }
.gauge .ring-bg { fill:none; stroke:var(--surface-2); stroke-width:6; }
.gauge .ring-v { fill:none; stroke-width:6; stroke-linecap:round;
transform:rotate(-90deg); transform-origin:center; }
.gauge b { font-size:1.3rem; letter-spacing:-.02em; }
.gauge span { display:block; font-size:.71rem; color:var(--ink-3); }
/* ── 대응 필요(playbook) ── */
.playbook {
border:1px solid rgba(251,191,36,.25); background: rgba(251,191,36,.05);
border-radius: var(--r-lg); padding: 16px 20px; margin-bottom: 18px;
}
.playbook summary { cursor:pointer; font-weight:700; font-size:.9375rem; list-style:none; display:flex; align-items:center; gap:10px; }
.playbook summary::-webkit-details-marker{display:none}
.playbook summary .chev { margin-left:auto; transition: transform .25s var(--spring); color:var(--ink-3); }
.playbook[open] summary .chev { transform: rotate(90deg); }
.playbook ol { margin: 12px 0 4px 20px; font-size:.8438rem; color:var(--ink-2); }
.playbook ol li { padding:3px 0; }
.pb-pill { font-size:.66rem; background:rgba(251,191,36,.15); color:#fde68a; padding:3px 9px; border-radius:999px; font-weight:600; }
/* ── 탭 페인 전환 ── */
.tabpane { display:none; }
.tabpane.active { display:block; animation: fadeUp .3s var(--spring); }
@keyframes fadeUp { from { opacity:0; transform: translateY(8px); } }
/* ── 차트 패널(추이 탭) ── */
.chart-panel {
background: var(--surface); border:1px solid var(--line);
border-radius: var(--r-lg); padding: 18px 20px; margin-bottom: 20px;
}
.chart-head { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; margin-bottom:12px; }
.legend { display:flex; gap:14px; font-size:.75rem; color:var(--ink-2); }
.legend i { display:inline-block; width:14px; height:3px; border-radius:2px; vertical-align:middle; margin-right:5px; }
.legend .sw-t { background:var(--temp); } .legend .sw-h { background:var(--hum); }
.legend .sw-w { background:var(--warn); } .legend .sw-l { background:var(--leak); }
.range-toggle { display:flex; gap:3px; padding:3px; background:var(--surface-2); border-radius:999px; }
.range-toggle a {
font-size:.75rem; padding:4px 13px; border-radius:999px; color:var(--ink-2);
transition: background .2s var(--spring), color .18s, transform .15s var(--spring);
}
.range-toggle a:active { transform: scale(.95); }
.range-toggle a.cur { background:var(--accent); color:#fff; }
.bigchart { width:100%; height:auto; display:block; }
.bigchart .gl, svg .gl { stroke: rgba(255,255,255,.06); }
.bigchart .ax { fill: var(--ink-3); font-size:10px; }
.bigchart .ax-t { fill: var(--temp); font-size:10px; }
.bigchart .ax-h { fill: var(--hum); font-size:10px; }
.bigchart .ln-t { stroke: var(--temp); fill:none; stroke-width:2; stroke-linejoin:round; stroke-linecap:round; }
.bigchart .ln-h { stroke: var(--hum); fill:none; stroke-width:2; stroke-dasharray:6 3; stroke-linejoin:round; stroke-linecap:round; }
.bigchart .thr { stroke-dasharray:2 4; stroke-width:1; opacity:.8; }
.bigchart .area-t { fill:url(#gt2); stroke:none; }
.peak-flag { fill: rgba(249,115,98,.18); }
.peak-label { fill:#fda49a; font-size:10px; font-weight:600; }
/* 통계 위젯 그리드 */
.widgets { display:grid; grid-template-columns: 1.4fr 1fr 1fr; gap:14px; }
.widget {
background: var(--surface); border:1px solid var(--line);
border-radius: var(--r-lg); padding: 16px 18px;
}
.widget h3 { font-size:.875rem; letter-spacing:-.01em; margin-bottom:10px; }
.widget h3 small { font-weight:400; color:var(--ink-3); font-size:.7rem; margin-left:6px; }
.donut-wrap { display:flex; align-items:center; gap:16px; }
.donut-wrap svg { width:110px; height:110px; }
.donut-wrap .d-track { fill:none; stroke:var(--surface-2); stroke-width:12; }
.donut-wrap .seg { fill:none; stroke-width:12; }
.donut-num { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; }
.donut-num b { font-size:1.4rem; letter-spacing:-.02em; }
.donut-num small { font-size:.66rem; color:var(--ink-3); }
.donut-ring { position:relative; }
.dl { display:flex; align-items:center; gap:7px; font-size:.75rem; color:var(--ink-2); padding:3px 0; }
.dl i { width:9px; height:9px; border-radius:3px; }
.dl .dl-c { margin-left:auto; font-weight:700; color:var(--ink); font-variant-numeric:tabular-nums; }
.board-row { display:flex; align-items:center; gap:10px; padding:6px 0; }
.board-rank { width:20px; height:20px; border-radius:6px; background:var(--surface-2); font-size:.7rem; font-weight:700;
display:flex; align-items:center; justify-content:center; color:var(--ink-2); }
.board-main { flex:1; }
.board-name { font-size:.78rem; margin-bottom:3px; }
.board-bar { height:5px; background:var(--surface-2); border-radius:999px; overflow:hidden; }
.board-bar span { display:block; height:100%; border-radius:999px;
background:linear-gradient(90deg,var(--accent),#a78bfa); }
.board-cnt { font-size:.8rem; font-weight:700; font-variant-numeric:tabular-nums; }
.board-cnt small { color:var(--ink-3); font-weight:400; }
/* ── 이력 테이블 ── */
.tablecard {
background: var(--surface); border:1px solid var(--line);
border-radius: var(--r-lg); overflow:hidden; margin-bottom:18px;
}
.tablecard header {
display:flex; align-items:center; gap:10px;
padding:14px 18px; border-bottom:1px solid var(--line);
font-weight:700; font-size:.9375rem; letter-spacing:-.01em;
background: rgba(255,255,255,.02); position: static;
}
.tablecard header small { font-weight:400; color:var(--ink-3); font-size:.75rem; }
.tablecard header .count-pill {
margin-left:auto; font-size:.69rem; font-weight:600; color:var(--ink-2);
background:var(--surface-2); padding:3px 10px; border-radius:999px;
}
.twrap { overflow-x:auto; }
table.t { width:100%; border-collapse:collapse; font-size:.8125rem; }
table.t th {
text-align:left; font-weight:600; font-size:.69rem; color:var(--ink-3);
letter-spacing:.04em; padding:10px 16px; border-bottom:1px solid var(--line);
white-space:nowrap;
}
table.t td { padding:10px 16px; border-bottom:1px solid rgba(255,255,255,.05);
font-variant-numeric:tabular-nums; white-space:nowrap; }
table.t tr:last-child td { border-bottom:none; }
table.t tbody tr { transition: background .15s; }
table.t tbody tr:hover { background: rgba(255,255,255,.03); }
table.t tr.hot { background: rgba(251,113,133,.06); }
table.t tr.hot:hover { background: rgba(251,113,133,.1); }
.pill {
display:inline-block; font-size:.69rem; font-weight:600; padding:2px 10px; border-radius:999px;
}
.pill.ok { background:rgba(52,211,153,.13); color:var(--ok); }
.pill.warn { background:rgba(251,191,36,.13); color:var(--warn); }
.pill.err { background:rgba(251,113,133,.13); color:var(--err); }
.pill.leak { background:rgba(96,165,250,.13); color:var(--leak); }
.pill.mut { background:var(--surface-2); color:var(--ink-2); }
.thumbs { display:flex; gap:6px; }
.thumb {
width:44px; height:34px; border-radius:6px; border:1px solid var(--line);
background:linear-gradient(135deg,#1c2444,#25305c);
display:flex; align-items:center; justify-content:center; font-size:.85rem;
transition: transform .18s var(--spring);
}
.thumb:hover { transform: scale(1.35); }
/* 통계 스탯 그리드 */
.stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:10px; }
.stat {
background:var(--surface-2); border-radius:var(--r-md); padding:12px 14px;
}
.stat span { display:block; font-size:.69rem; color:var(--ink-3); }
.stat strong { font-size:1.25rem; letter-spacing:-.02em; font-variant-numeric:tabular-nums; }
.stat strong.w { color:var(--warn); } .stat strong.e { color:var(--err); }
/* 반응형 */
@media (max-width: 980px) { .kpis { grid-template-columns: repeat(3,1fr); } }
@media (max-width: 640px) {
.container { padding: 16px; }
.kpis { grid-template-columns: repeat(2,1fr); }
.hero { flex-direction:column; align-items:flex-start; }
.hero .clock { text-align:left; }
.tabs { width:100%; overflow-x:auto; }
}
/* 접근성: 모션 최소화 — 슬라이드/펄스 제거, 페이드만 유지 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: opacity .2s !important; }
}
@media (prefers-reduced-transparency: reduce) {
.topbar { background: var(--bg-2); backdrop-filter:none; -webkit-backdrop-filter:none; }
}
</style>
</head>
<body>
<!-- 상단 글래스 툴바 -->
<header class="topbar">
<div class="brand"><span class="dot"></span>누수·온습도 관제 <small>v3</small></div>
<div class="spacer"></div>
<a class="top-link" href="#">설치 점검</a>
<a class="top-link" href="#">월간 보고</a>
<a class="top-link" href="#">보안 증적</a>
<div class="refresh-pill">
<svg class="refresh-ring" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/></svg>
30초 갱신
<button type="button" onclick="this.textContent=this.textContent==='⏸'?'▶':'⏸'">⏸</button>
</div>
<a class="top-link" href="#">로그아웃</a>
</header>
<main class="container">
<!-- 시스템 상태 히어로: 판정 1문장, 경보 시 .err 로 전환 -->
<section class="hero err">
<div>
<h1>⚠ 누수 감지, 즉시 확인 필요</h1>
<p class="sub">3번 센서(기계실 B) 09:42 누수 확정 · SMS 4명 발송 완료 · 나머지 3개소 정상</p>
</div>
<div class="clock">
<b>2026-07-16 10:05:12</b>
마지막 수신 32초 전
</div>
</section>
<!-- 알림 스트립 -->
<a class="strip" href="#">
<span class="ico">🚨</span>
<strong>미복구 누수 사고 1건</strong>
<span>기계실 B: 현장 조치·복구 확인 필요</span>
<em>실시간 현황 →</em>
</a>
<a class="strip warn" href="#">
<span class="ico">✉️</span>
<strong>SMS 잔여 42건</strong>
<span>경보 임계(50건) 이하, 충전 권장</span>
<em>시스템 점검 →</em>
</a>
<!-- KPI 스트립 -->
<nav class="kpis" aria-label="핵심 현황">
<a class="kpi err" href="#"><span class="lbl">경보 센서</span><div class="val">1<small>개</small></div><div class="sub">누수 1 · 온습도 0</div></a>
<a class="kpi ok" href="#"><span class="lbl">온라인</span><div class="val">4<small>/4</small></div><div class="sub">가동률 100%</div></a>
<a class="kpi err" href="#"><span class="lbl">미복구 누수</span><div class="val">1<small>건</small></div><div class="sub">09:42 발생</div></a>
<a class="kpi warn" href="#"><span class="lbl">오늘 경보</span><div class="val">3<small>건</small></div><div class="sub">최근 09:42</div></a>
<a class="kpi warn" href="#"><span class="lbl">SMS 잔여</span><div class="val">42<small>건</small></div><div class="sub">오늘 4건 발송 · 실패 0</div></a>
<a class="kpi ok" href="#"><span class="lbl">중요 진단</span><div class="val">0<small>건</small></div><div class="sub">자가진단 정상</div></a>
</nav>
<!-- 대응 필요: 접이식 플레이북 -->
<details class="playbook" open>
<summary><span class="pb-pill">경보</span> 대응 필요: 3번 센서(기계실 B) 누수 확정 <span class="chev">›</span></summary>
<ol>
<li>현장으로 이동해 누수 지점·규모를 확인합니다.</li>
<li>밸브 차단 등 1차 조치 후 사진을 촬영해 업로드합니다.</li>
<li>센서 건조 확인 후 복구 SMS 자동 발송을 확인합니다.</li>
<li>담당자: 시설 김OO <b>010-****-1234</b></li>
</ol>
</details>
<!-- 세그먼트 탭바 -->
<nav class="tabs" aria-label="대시보드 섹션">
<a class="tab active" href="#" data-tab="live">실시간 현황<span class="badge">2</span></a>
<a class="tab" href="#" data-tab="trend">추이·통계</a>
<a class="tab" href="#" data-tab="history">이력 조회</a>
<a class="tab" href="#" data-tab="system">시스템 점검</a>
<a class="tab" href="#" data-tab="print">🖨 인쇄</a>
</nav>
<!-- ═══════════ 탭: 실시간 현황 ═══════════ -->
<section class="tabpane active" id="pane-live">
<h2 class="sec">센서 현황 <small>· 4개소</small></h2>
<div class="grid">
<!-- 누수 경보 카드 -->
<div class="card leak pulse">
<div class="c-head"><strong>3번 센서</strong><span class="chip-type leak">누수</span></div>
<div class="c-loc">기계실 B / pi-c-01</div>
<div class="status-line leak"><span class="status-dot"></span>누수 감지 (09:42 확정)</div>
<div class="vals">
<div class="v"><div class="n" style="color:var(--leak)">23<small>분</small></div><div class="l">경과 시간</div></div>
<div class="v"><div class="n">4<small>명</small></div><div class="l">SMS 발송</div></div>
</div>
<div class="fchips">
<span class="fchip over">미복구, 현장 조치 필요</span>
</div>
<div class="c-foot"><span>최종확인 10:04:58</span><span>온라인</span></div>
<svg class="wave" viewBox="0 0 300 46" preserveAspectRatio="none">
<path d="M0 28 Q 25 18 50 28 T 100 28 T 150 28 T 200 28 T 250 28 T 300 28 V46 H0 Z" fill="#60a5fa"/>
</svg>
</div>
<!-- 온습도 정상(예보 여유) -->
<div class="card">
<div class="c-head"><strong>1번 센서</strong><span class="chip-type">온습도</span></div>
<div class="c-loc">전산실 A / pi-a-01</div>
<div class="status-line ok"><span class="status-dot"></span>정상</div>
<div class="vals">
<div class="v temp"><div class="n">24.3<small>°C</small></div><div class="l">온도</div></div>
<div class="v hum"><div class="n">51<small>%</small></div><div class="l">습도</div></div>
</div>
<svg class="spark" viewBox="0 0 300 34" preserveAspectRatio="none">
<polyline class="t" points="0,20 30,19 60,21 90,18 120,17 150,19 180,16 210,17 240,15 270,16 300,14"/>
<polyline class="h" points="0,26 30,25 60,27 90,26 120,24 150,25 180,23 210,24 240,22 270,23 300,22"/>
</svg>
<div class="fchips">
<span class="fchip">고온 여유 5.7°C</span><span class="fchip">고습 여유 19%</span>
</div>
<div class="c-foot"><span>측정 10:00:04</span><span>10분 주기</span></div>
</div>
<!-- 온습도 임계 접근(예보 경고) -->
<div class="card">
<div class="c-head"><strong>2번 센서</strong><span class="chip-type">온습도</span></div>
<div class="c-loc">서버실 / pi-b-01</div>
<div class="status-line ok"><span class="status-dot"></span>정상 (상승 추세)</div>
<div class="vals">
<div class="v temp"><div class="n hot">28.9<small>°C</small></div><div class="l">온도</div></div>
<div class="v hum"><div class="n">44<small>%</small></div><div class="l">습도</div></div>
</div>
<svg class="spark" viewBox="0 0 300 34" preserveAspectRatio="none">
<polyline class="t" points="0,26 30,25 60,24 90,22 120,21 150,18 180,16 210,13 240,11 270,9 300,7"/>
<polyline class="h" points="0,24 30,25 60,24 90,25 120,26 150,25 180,26 210,25 240,26 270,25 300,26"/>
</svg>
<div class="fchips">
<span class="fchip near">↗ ~35분 뒤 고온 도달</span><span class="fchip near">급상승 +1.2°C/10분</span>
</div>
<div class="c-foot"><span>측정 10:00:11</span><span>1시간 주기</span></div>
</div>
<!-- 누수 정상 카드 -->
<div class="card">
<div class="c-head"><strong>4번 센서</strong><span class="chip-type leak">누수</span></div>
<div class="c-loc">배관실 / pi-c-02</div>
<div class="status-line ok"><span class="status-dot"></span>정상 (건조)</div>
<div class="vals">
<div class="v"><div class="n" style="color:var(--ok)">37<small>일</small></div><div class="l">무사고 연속</div></div>
<div class="v"><div class="n">100<small>%</small></div><div class="l">24h 가동률</div></div>
</div>
<div class="fchips"><span class="fchip">마지막 점검 06-09</span></div>
<div class="c-foot"><span>최종확인 10:05:02</span><span>온라인</span></div>
</div>
</div>
<h2 class="sec">채널·오늘 요약</h2>
<div class="panels">
<section class="panel">
<h3>SMS 채널 상태</h3>
<div class="gauge">
<svg viewBox="0 0 64 64">
<circle class="ring-bg" cx="32" cy="32" r="26"/>
<circle class="ring-v" cx="32" cy="32" r="26" stroke="var(--warn)"
stroke-dasharray="163" stroke-dashoffset="95"/>
</svg>
<div><b>42건</b><span>잔여 / 경보 임계 50건</span></div>
</div>
<dl class="rows">
<div><dt>마지막 성공 발송</dt><dd>09:42:18 (누수 경보)</dd></div>
<div><dt>오늘 발송 / 실패</dt><dd>4건 / <span class="ok-c">0건</span></dd></div>
</dl>
<p class="note">SMS는 유일한 경보 채널입니다. 임계 이하로 내려가면 즉시 충전하세요.</p>
</section>
<section class="panel">
<h3>오늘 경보 요약</h3>
<dl class="rows">
<div><dt>오늘 경보 횟수</dt><dd class="err-c">3건</dd></div>
<div><dt>오늘 최근 경보</dt><dd>09:42 (누수 · 3번)</dd></div>
<div><dt>가동률</dt><dd class="ok-c">100% (4/4 온라인)</dd></div>
</dl>
<p class="note">7일·30일 추이는 추이·통계 탭에서 확인하세요.</p>
</section>
<section class="panel">
<h3>미복구 누수 사고 · 1건</h3>
<dl class="rows">
<div><dt>#41 · 3번 센서</dt><dd class="err-c">미복구</dd></div>
<div><dt>위치</dt><dd>기계실 B</dd></div>
<div><dt>감지 시각</dt><dd>2026-07-16 09:42</dd></div>
</dl>
<p class="note">사진·복구 시각 등 전체 이력은 이력 조회 탭에서 확인하세요.</p>
</section>
</div>
</section>
<!-- ═══════════ 탭: 추이·통계 ═══════════ -->
<section class="tabpane" id="pane-trend">
<h2 class="sec">온습도 추이 <small>· 1번 센서(전산실 A) 최근 48개</small></h2>
<div class="chart-panel">
<div class="chart-head">
<div class="legend">
<span><i class="sw-t"></i>온도(°C)</span>
<span><i class="sw-h"></i>습도(%)</span>
</div>
<div class="range-toggle" role="group" aria-label="추이 기간 선택">
<a href="#" class="cur">오늘</a><a href="#">3일</a><a href="#">7일</a>
</div>
</div>
<svg class="bigchart" viewBox="0 0 920 280" role="img" aria-label="온습도 추이 라인 차트">
<defs>
<linearGradient id="gt2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f97362" stop-opacity=".14"/>
<stop offset="1" stop-color="#f97362" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- 그리드 + 좌(온도)/우(습도) 눈금 -->
<g>
<line class="gl" x1="44" y1="16" x2="876" y2="16"/>
<line class="gl" x1="44" y1="73" x2="876" y2="73"/>
<line class="gl" x1="44" y1="130" x2="876" y2="130"/>
<line class="gl" x1="44" y1="187" x2="876" y2="187"/>
<line class="gl" x1="44" y1="244" x2="876" y2="244"/>
<text class="ax-t" x="38" y="19" text-anchor="end">32</text>
<text class="ax-t" x="38" y="76" text-anchor="end">28</text>
<text class="ax-t" x="38" y="133" text-anchor="end">24</text>
<text class="ax-t" x="38" y="190" text-anchor="end">20</text>
<text class="ax-t" x="38" y="247" text-anchor="end">16</text>
<text class="ax-h" x="882" y="19" >80</text>
<text class="ax-h" x="882" y="76" >65</text>
<text class="ax-h" x="882" y="133" >50</text>
<text class="ax-h" x="882" y="190" >35</text>
<text class="ax-h" x="882" y="247" >20</text>
<text class="ax" x="44" y="270">07-16 00:00</text>
<text class="ax" x="460" y="270" text-anchor="middle">07-16 05:00</text>
<text class="ax" x="876" y="270" text-anchor="end">07-16 10:00</text>
</g>
<!-- 임계선: 고온 30°C / 고습 70% -->
<line class="thr" x1="44" y1="45" x2="876" y2="45" stroke="var(--temp)"/>
<text class="ax-t" x="48" y="41">고온 30°C</text>
<line class="thr" x1="44" y1="54" x2="876" y2="54" stroke="var(--hum)"/>
<text class="ax-h" x="872" y="50" text-anchor="end">고습 70%</text>
<!-- 온도 면적 + 라인 -->
<path class="area-t" d="M44,160 C120,158 180,150 260,146 C340,142 400,148 480,138 C560,128 620,120 700,106 C780,94 830,88 876,80 L876,244 L44,244 Z"/>
<path class="ln-t" d="M44,160 C120,158 180,150 260,146 C340,142 400,148 480,138 C560,128 620,120 700,106 C780,94 830,88 876,80" fill="none"/>
<!-- 습도 라인(파선) -->
<path class="ln-h" d="M44,128 C120,132 180,126 260,130 C340,134 400,128 480,132 C560,136 620,130 700,134 C780,130 830,133 876,131" fill="none"/>
<!-- 피크 마커 -->
<line class="gl" x1="876" y1="80" x2="876" y2="16"/>
<rect class="peak-flag" x="796" y="2" width="76" height="15" rx="4"/>
<text class="peak-label" x="834" y="13" text-anchor="middle">피크 27.4°C</text>
<circle cx="876" cy="80" r="4" fill="var(--temp)"/>
<text class="ax-t" x="866" y="72" text-anchor="end">27.4°C</text>
<circle cx="876" cy="131" r="3" fill="var(--hum)"/>
<text class="ax-h" x="866" y="150" text-anchor="end">50.2%</text>
</svg>
</div>
<h2 class="sec">경보·가동 통계</h2>
<div class="widgets">
<!-- 경보 발생 추이 7일 -->
<div class="widget">
<h3>경보 발생 추이 <small>최근 7일 · 유형별</small></h3>
<div class="legend" style="margin-bottom:8px">
<span><i class="sw-w"></i>온습도 경보</span>
<span><i class="sw-l"></i>누수 감지</span>
</div>
<svg viewBox="0 0 640 190" style="width:100%;height:auto" role="img" aria-label="온습도 경보 vs 누수 감지 7일 추이">
<line class="gl" x1="40" y1="20" x2="624" y2="20" stroke="rgba(255,255,255,.06)"/>
<line class="gl" x1="40" y1="88" x2="624" y2="88" stroke="rgba(255,255,255,.06)"/>
<line class="gl" x1="40" y1="156" x2="624" y2="156" stroke="rgba(255,255,255,.06)"/>
<text x="34" y="23" text-anchor="end" font-size="10" fill="var(--ink-3)">6</text>
<text x="34" y="91" text-anchor="end" font-size="10" fill="var(--ink-3)">3</text>
<text x="34" y="159" text-anchor="end" font-size="10" fill="var(--ink-3)">0</text>
<polyline points="40,156 137,133 234,156 331,110 428,133 525,156 624,88"
fill="none" stroke="var(--leak)" stroke-width="2" stroke-dasharray="6 3"
stroke-linejoin="round" stroke-linecap="round"/>
<polyline points="40,110 137,88 234,133 331,42 428,110 525,133 624,110"
fill="none" stroke="var(--warn)" stroke-width="2"
stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="331" cy="42" r="3.4" fill="var(--warn)"/>
<text x="40" y="182" font-size="9" fill="var(--ink-3)">07-10</text>
<text x="331" y="182" font-size="9" fill="var(--ink-3)" text-anchor="middle">07-13</text>
<text x="624" y="182" font-size="9" fill="var(--ink-3)" text-anchor="end">07-16</text>
</svg>
</div>
<!-- 경보 종류 도넛 30일 -->
<div class="widget">
<h3>경보 종류 <small>최근 30일</small></h3>
<div class="donut-wrap">
<div class="donut-ring">
<svg viewBox="0 0 120 120" role="img" aria-label="경보 종류 구성 총 14건">
<circle class="d-track" cx="60" cy="60" r="47" pathLength="100"/>
<g transform="rotate(-90 60 60)">
<circle class="seg" cx="60" cy="60" r="47" pathLength="100" stroke="var(--warn)"
stroke-dasharray="43 100" stroke-dashoffset="0"/>
<circle class="seg" cx="60" cy="60" r="47" pathLength="100" stroke="var(--leak)"
stroke-dasharray="29 100" stroke-dashoffset="-43"/>
<circle class="seg" cx="60" cy="60" r="47" pathLength="100" stroke="var(--err)"
stroke-dasharray="21 100" stroke-dashoffset="-72"/>
<circle class="seg" cx="60" cy="60" r="47" pathLength="100" stroke="var(--ink-3)"
stroke-dasharray="7 100" stroke-dashoffset="-93"/>
</g>
</svg>
<div class="donut-num"><b>14</b><small>건</small></div>
</div>
<div style="flex:1">
<span class="dl"><i style="background:var(--warn)"></i>고온<span class="dl-c">6</span></span>
<span class="dl"><i style="background:var(--leak)"></i>누수 감지<span class="dl-c">4</span></span>
<span class="dl"><i style="background:var(--err)"></i>고습<span class="dl-c">3</span></span>
<span class="dl"><i style="background:var(--ink-3)"></i>저온<span class="dl-c">1</span></span>
</div>
</div>
<p class="note" style="font-size:.66rem;color:var(--ink-3);margin-top:10px">단위: 온습도는 비정상 측정 건수, 누수는 감지 이벤트 건수</p>
</div>
<!-- 가동률 게이지 + 리더보드 -->
<div class="widget">
<h3>가동률 <small>온라인 센서</small></h3>
<div class="donut-wrap" style="margin-bottom:12px">
<div class="donut-ring">
<svg viewBox="0 0 120 120" role="img" aria-label="가동률 100%">
<g transform="rotate(-90 60 60)">
<circle class="d-track" cx="60" cy="60" r="47" pathLength="100"/>
<circle class="seg" cx="60" cy="60" r="47" pathLength="100" stroke="var(--ok)"
stroke-dasharray="100 100" stroke-linecap="round"/>
</g>
</svg>
<div class="donut-num"><b>100%</b><small>4 / 4 온라인</small></div>
</div>
<div style="flex:1">
<div style="font-size:.7rem;color:var(--ink-3);margin-bottom:6px">경보 최다 센서 · 30일</div>
<div class="board-row">
<span class="board-rank">1</span>
<div class="board-main"><div class="board-name">2번 센서</div>
<div class="board-bar"><span style="width:100%"></span></div></div>
<span class="board-cnt">7<small>건</small></span>
</div>
<div class="board-row">
<span class="board-rank">2</span>
<div class="board-main"><div class="board-name">3번 센서</div>
<div class="board-bar"><span style="width:57%"></span></div></div>
<span class="board-cnt">4<small>건</small></span>
</div>
<div class="board-row">
<span class="board-rank">3</span>
<div class="board-main"><div class="board-name">1번 센서</div>
<div class="board-bar"><span style="width:43%"></span></div></div>
<span class="board-cnt">3<small>건</small></span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ═══════════ 탭: 이력 조회 ═══════════ -->
<section class="tabpane" id="pane-history">
<div class="tablecard">
<header>누수 사고 이력 <small>사진·복구 대조</small><span class="count-pill">최근 20건</span></header>
<div class="twrap">
<table class="t">
<thead><tr><th>#</th><th>센서</th><th>위치</th><th>감지 시각</th><th>복구 시각</th><th>사진</th></tr></thead>
<tbody>
<tr class="hot">
<td>41</td><td>3번 센서</td><td>기계실 B</td><td>2026-07-16 09:42</td>
<td><span class="pill leak">미복구</span></td>
<td><div class="thumbs"><a class="thumb" href="#">📷</a><a class="thumb" href="#">📷</a></div></td>
</tr>
<tr>
<td>40</td><td>4번 센서</td><td>배관실</td><td>2026-06-09 14:03</td>
<td>2026-06-09 15:21</td>
<td><div class="thumbs"><a class="thumb" href="#">📷</a></div></td>
</tr>
<tr>
<td>39</td><td>3번 센서</td><td>기계실 B</td><td>2026-05-27 03:18</td>
<td>2026-05-27 08:40</td>
<td><div class="thumbs"><a class="thumb" href="#">📷</a><a class="thumb" href="#">📷</a><a class="thumb" href="#">📷</a></div></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="tablecard">
<header>최근 온습도 측정 <span class="count-pill">30건 기준</span></header>
<div class="twrap">
<table class="t">
<thead><tr><th>측정시각</th><th>센서</th><th>위치</th><th>온도</th><th>습도</th><th>상태</th></tr></thead>
<tbody>
<tr><td>10:00:11</td><td>2번 센서</td><td>서버실</td><td>28.9°C</td><td>44%</td><td><span class="pill ok">정상</span></td></tr>
<tr><td>10:00:04</td><td>1번 센서</td><td>전산실 A</td><td>24.3°C</td><td>51%</td><td><span class="pill ok">정상</span></td></tr>
<tr class="hot"><td>09:00:09</td><td>2번 센서</td><td>서버실</td><td>30.6°C</td><td>43%</td><td><span class="pill err">고온</span></td></tr>
<tr><td>09:00:03</td><td>1번 센서</td><td>전산실 A</td><td>24.1°C</td><td>52%</td><td><span class="pill ok">정상</span></td></tr>
<tr><td>08:00:07</td><td>2번 센서</td><td>서버실</td><td>29.2°C</td><td>45%</td><td><span class="pill ok">정상</span></td></tr>
</tbody>
</table>
</div>
</div>
<div class="tablecard">
<header>센서 이벤트 로그 <span class="count-pill">30건 기준</span></header>
<div class="twrap">
<table class="t">
<thead><tr><th>#</th><th>시각</th><th>센서</th><th>위치</th><th>이벤트</th></tr></thead>
<tbody>
<tr class="hot"><td>212</td><td>09:42:02</td><td>3번 센서</td><td>기계실 B</td><td><span class="pill leak">누수감지</span></td></tr>
<tr><td>211</td><td>09:00:00</td><td>3번 센서</td><td>기계실 B</td><td>정기보고</td></tr>
<tr><td>210</td><td>09:00:00</td><td>4번 센서</td><td>배관실</td><td>정기보고</td></tr>
<tr><td>209</td><td>06:12:44</td><td>4번 센서</td><td>배관실</td><td>장비시작</td></tr>
<tr><td>208</td><td>2026-06-09 15:21</td><td>4번 센서</td><td>배관실</td><td><span class="pill ok">누수복구</span></td></tr>
</tbody>
</table>
</div>
</div>
<div class="tablecard">
<header>SMS 발송 이력 <span class="count-pill">최근 20건</span></header>
<div class="twrap">
<table class="t">
<thead><tr><th>#</th><th>발송시각</th><th>센서</th><th>수신자</th><th>상태</th><th>결과</th></tr></thead>
<tbody>
<tr><td>88</td><td>09:42:18</td><td>3번 센서</td><td>010-****-1234 외 3명</td><td><span class="pill ok">성공</span></td><td style="color:var(--ink-3)">누수 경보 발송</td></tr>
<tr><td>87</td><td>09:00:31</td><td>2번 센서</td><td>010-****-5678 외 1명</td><td><span class="pill ok">성공</span></td><td style="color:var(--ink-3)">고온 경보 발송</td></tr>
<tr><td>86</td><td>2026-07-15 22:10</td><td>2번 센서</td><td>010-****-5678 외 1명</td><td><span class="pill ok">성공</span></td><td style="color:var(--ink-3)">고온 복구 안내</td></tr>
<tr class="hot"><td>85</td><td>2026-07-14 11:02</td><td>1번 센서</td><td>010-****-1234</td><td><span class="pill err">실패</span></td><td style="color:var(--ink-3)">잔액 부족 오류</td></tr>
</tbody>
</table>
</div>
</div>
<div class="panels" style="margin-bottom:18px">
<section class="panel">
<h3>30일 평균 온습도 <span style="font-weight:400;color:var(--ink-3);font-size:.75rem">· 센서별</span></h3>
<dl class="rows">
<div><dt>1번 센서 (전산실 A)</dt><dd>24.6°C / 50.8% · 4,120건</dd></div>
<div><dt>2번 센서 (서버실)</dt><dd>27.9°C / 44.2% · 698건</dd></div>
<div><dt>3번 센서 (기계실 B)</dt><dd>22.1°C / 58.3% · 4,088건</dd></div>
</dl>
<p class="note">전 센서 혼합 평균은 위치별 환경 차이를 뭉개므로 센서별로 분리 표기.</p>
</section>
<section class="panel">
<h3>30일 SMS 발송 통계</h3>
<div class="stats">
<div class="stat"><span>총 발송</span><strong>31건</strong></div>
<div class="stat"><span>온습도 경보</span><strong class="w">12건</strong></div>
<div class="stat"><span>누수감지</span><strong class="e">8건</strong></div>
<div class="stat"><span>복구</span><strong>7건</strong></div>
<div class="stat"><span>오프라인</span><strong>3건</strong></div>
<div class="stat"><span>발송 실패</span><strong class="e">1건</strong></div>
</div>
<p class="note">종류별 상세 집계와 월별 분석은 월간 보고에서 확인하세요.</p>
</section>
</div>
</section>
<!-- ═══════════ 탭: 시스템/인쇄 (플레이스홀더) ═══════════ -->
<section class="tabpane" id="pane-system">
<div class="panel" style="text-align:center;padding:48px;color:var(--ink-3)">시스템 점검 탭 (목업 범위 외)</div>
</section>
<section class="tabpane" id="pane-print">
<div class="panel" style="text-align:center;padding:48px;color:var(--ink-3)">인쇄용 전체 보기 (목업 범위 외)</div>
</section>
</main>
<script>
(function () {
'use strict';
var tabs = document.querySelectorAll('.tab[data-tab]');
tabs.forEach(function (t) {
t.addEventListener('click', function (e) {
e.preventDefault();
tabs.forEach(function (o) { o.classList.remove('active'); });
t.classList.add('active');
document.querySelectorAll('.tabpane').forEach(function (p) { p.classList.remove('active'); });
var pane = document.getElementById('pane-' + t.dataset.tab);
if (pane) { pane.classList.add('active'); }
});
});
})();
</script>
</body>
</html>