- tools/make_deploy_bundles.py: PMS7003·Zero2W 디스플레이 노드 번들 생성 반영(폐쇄망 동봉) - tools/harden_pi.sh: 하드닝 스크립트 갱신 - docs/superpowers: 노드 센서 통합·스모크 파일럿·대시보드 다크 설계/계획 문서 - docs/claude-config: IoT 납품·검증 스킬/룰 참조본 - review/: 프로젝트 리뷰·UI v2 목업
912 lines
46 KiB
HTML
912 lines
46 KiB
HTML
<!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>
|