참고 대시보드 미학(대형 숫자 KPI·플랫 카드)으로 재테마하고, 통합 센서(누수+온습도+미세먼지)를 유형 통합 그리드로 표시. PM2.5/PM10 카드·트렌드·이력을 추가. - php/assets/dash-v2.css: 후행 캐스케이드 오버라이드(기존 파일 무수정 가역 방식) - php/api/status_summary.php: 상태 요약 폴링 엔드포인트(try/catch·LIMIT·제네릭 에러 JSON) - dashboard_data.php: PM 카드 데이터·결로/예보 메트릭 정합 - tabs/live·trend·history: 미세먼지 표시·추세·이력 반영
237 lines
13 KiB
CSS
237 lines
13 KiB
CSS
/* =============================================================================
|
|
* dash-v2.css — dashboard.php 전용 오버레이 (app.css 뒤에 로드)
|
|
* 목업 review/ui_mockup_v2.html 반영: 글래스 툴바 · 스프링 모션 · 세그먼트 탭 ·
|
|
* 결합 센서 카드(누수+온습도) · 카운트다운 링 · 카드형 테이블 헤더.
|
|
* app.css 는 7페이지 공용이므로 건드리지 않는다. 인쇄 규칙은 app.css 것을 계승.
|
|
* 폐쇄망: 외부 리소스 0.
|
|
* ============================================================================= */
|
|
|
|
:root {
|
|
--spring: cubic-bezier(.32,.72,.28,1); /* 감쇠 스프링 근사 */
|
|
}
|
|
|
|
/* ── 글래스 툴바: 반투명 재질 + sticky, 콘텐츠가 아래로 스크롤 ─────────────── */
|
|
header {
|
|
background: rgba(20,26,61,.78);
|
|
backdrop-filter: blur(18px) saturate(160%);
|
|
-webkit-backdrop-filter: blur(18px) saturate(160%);
|
|
}
|
|
.top-actions a {
|
|
transition: background .18s var(--spring), color .15s, border-color .15s, transform .15s var(--spring);
|
|
}
|
|
.top-actions a:active { transform: scale(.96); }
|
|
|
|
/* ── 새로고침 카운트다운 링 ─────────────────────────────────────────────────── */
|
|
.refresh { display: inline-flex; align-items: center; gap: 8px; }
|
|
.refresh-ring { width: 15px; height: 15px; flex: none; }
|
|
.refresh-ring circle {
|
|
fill: none; stroke: var(--primary-600); stroke-width: 2.5;
|
|
stroke-dasharray: 38; stroke-linecap: round;
|
|
transform: rotate(-90deg); transform-origin: center;
|
|
animation: dash-countdown 30s linear infinite;
|
|
}
|
|
@keyframes dash-countdown { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 38; } }
|
|
.refresh.paused .refresh-ring circle { animation-play-state: paused; opacity: .35; }
|
|
.refresh-toggle { transition: transform .15s var(--spring), background .15s; }
|
|
.refresh-toggle:active { transform: scale(.93); }
|
|
|
|
/* ── 세그먼트 탭바(iOS segmented control) ───────────────────────────────────── */
|
|
.tabbar { border-radius: var(--r-pill); padding: 4px; gap: 3px; }
|
|
.tabbar .tab-link {
|
|
border-radius: var(--r-pill); padding: 8px 18px;
|
|
transition: background .22s var(--spring), color .15s, transform .15s var(--spring);
|
|
}
|
|
.tabbar .tab-link:active { transform: scale(.96); }
|
|
.tabbar .tab-link.active {
|
|
color: #fff;
|
|
background: linear-gradient(135deg, var(--primary), #8f6bff);
|
|
box-shadow: 0 4px 14px rgba(108,123,255,.35);
|
|
}
|
|
.tabbar .tab-link.active::after { content: none; }
|
|
.tabbar .tab-link.active .tab-badge.crit { background: rgba(255,255,255,.22); color: #fff; }
|
|
.tabbar .tab-print { border-radius: var(--r-pill); }
|
|
|
|
/* ── 인터랙션 공통: 즉각 반응(:active) + 부상(hover) 스프링 ─────────────────── */
|
|
.kpi, .sensor-card, .leak-status-card, .alert-strip, .quick-tile {
|
|
transition: transform .2s var(--spring), border-color .2s, box-shadow .2s, background .2s;
|
|
}
|
|
.kpi:hover, .sensor-card:hover, .leak-status-card:hover {
|
|
transform: translateY(-3px); box-shadow: var(--shadow-lg);
|
|
}
|
|
.kpi:active, .sensor-card:active, .alert-strip:active { transform: translateY(-1px) scale(.985); }
|
|
.alert-strip:hover { transform: translateX(3px); }
|
|
|
|
/* ── 결합 센서 카드 (누수+온습도) ────────────────────────────────────────────
|
|
상태 우선순위: 오프라인 > 누수 확정 > 확인중 > 온습도 경보 > 정상 */
|
|
.sensor-card { position: relative; overflow: hidden; }
|
|
.sensor-card::before {
|
|
content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
|
|
background: var(--normal); opacity: .85;
|
|
}
|
|
.sensor-card.alert::before { background: var(--leak); }
|
|
.sensor-card.leak::before { background: var(--chart-hum); }
|
|
.sensor-card.pending::before { background: var(--warn); }
|
|
.sensor-card.offline::before { background: var(--offline); }
|
|
.sensor-card.offline { opacity: .78; }
|
|
.sensor-card.leak {
|
|
border-color: rgba(63,199,230,.4);
|
|
background: linear-gradient(180deg, rgba(63,199,230,.10), transparent 60%), var(--surface);
|
|
animation: dash-leak-glow 1.8s ease-in-out infinite;
|
|
}
|
|
@keyframes dash-leak-glow {
|
|
50% { box-shadow: 0 0 26px rgba(63,199,230,.30); }
|
|
}
|
|
|
|
/* 유형 칩 2개(결합) 나란히 */
|
|
.sc-types { display: flex; gap: 4px; flex: none; }
|
|
|
|
/* 상태 라인: 점 + 텍스트 (badge-row 대체 — 기존 배지도 유지 호환) */
|
|
.status-line { display: flex; align-items: center; gap: 7px; font-size: .8125rem; font-weight: 700; margin-top: var(--sp-2); }
|
|
.status-line .sdot { width: 8px; height: 8px; border-radius: 50%; background: var(--normal); box-shadow: 0 0 8px var(--normal); flex: none; }
|
|
.status-line.ok { color: var(--normal-ink); }
|
|
.status-line.err { color: var(--leak-ink); }
|
|
.status-line.err .sdot { background: var(--leak); box-shadow: 0 0 8px var(--leak); animation: dash-pulse 1.2s ease-in-out infinite; }
|
|
.status-line.leakst { color: var(--cold-ink); }
|
|
.status-line.leakst .sdot { background: var(--chart-hum); box-shadow: 0 0 8px var(--chart-hum); animation: dash-pulse 1.2s ease-in-out infinite; }
|
|
.status-line.pend { color: var(--warn-ink); }
|
|
.status-line.pend .sdot { background: var(--warn); box-shadow: 0 0 8px var(--warn); }
|
|
.status-line.off { color: var(--offline-ink); }
|
|
.status-line.off .sdot { background: var(--offline); box-shadow: none; }
|
|
@keyframes dash-pulse { 50% { opacity: .4; } }
|
|
|
|
/* 온습도 큰 수치 2열 */
|
|
.vals { display: flex; gap: var(--sp-5); margin-top: var(--sp-3); }
|
|
.vals .v { flex: 1; min-width: 0; }
|
|
.vals .v .n {
|
|
font-size: 1.7rem; font-weight: 800; letter-spacing: -.03em; line-height: 1.08;
|
|
font-variant-numeric: tabular-nums; color: var(--ink);
|
|
}
|
|
.vals .v .n small { font-size: .85rem; color: var(--ink-2); font-weight: 600; letter-spacing: 0; }
|
|
.vals .v .l { font-size: .7rem; color: var(--ink-3); margin-top: 2px; }
|
|
.vals .v.temp .n { color: var(--chart-temp); }
|
|
.vals .v.hum .n { color: var(--chart-hum); }
|
|
.vals .v .n.hot { text-shadow: 0 0 14px rgba(255,138,92,.5); }
|
|
.vals .v .n.cold { color: var(--cold-ink); }
|
|
.vals .v .n.humid { text-shadow: 0 0 14px rgba(255,178,62,.5); }
|
|
.vals .v .n.dry { color: var(--dry-ink); }
|
|
|
|
/* 24h 스파크라인 */
|
|
.spark24 { width: 100%; height: 34px; display: block; margin-top: var(--sp-2); }
|
|
.spark24 polyline { fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
.spark24 .sp-t { stroke: var(--chart-temp); opacity: .85; }
|
|
.spark24 .sp-h { stroke: var(--chart-hum); opacity: .85; stroke-dasharray: 5 3; }
|
|
|
|
/* 누수 행(결합 카드 내부): 경과·SMS·사진 */
|
|
.leak-row {
|
|
display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
|
|
margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line-2);
|
|
}
|
|
.leak-row .leak-fact { font-size: .8125rem; color: var(--ink-2); font-variant-numeric: tabular-nums; }
|
|
.leak-row .leak-fact b { color: var(--ink); font-weight: 800; }
|
|
.leak-row .leak-thumbs { margin-left: auto; }
|
|
.leak-row .leak-thumbs img {
|
|
width: 46px; height: 36px; object-fit: cover;
|
|
transition: transform .18s var(--spring);
|
|
}
|
|
.leak-row .leak-thumbs a:hover img { transform: scale(1.5); }
|
|
|
|
/* 누수 카드 하단 물결(장식) */
|
|
.leak-wave { position: absolute; left: 0; right: 0; bottom: 0; height: 42px; opacity: .14; pointer-events: none; }
|
|
.leak-wave path { fill: var(--chart-hum); }
|
|
|
|
/* 카드 푸터: 최종확인 + 보고 주기 */
|
|
.sc-foot {
|
|
display: flex; justify-content: space-between; gap: var(--sp-2);
|
|
font-size: .72rem; color: var(--ink-3); margin-top: var(--sp-3);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.sc-foot .stale { color: var(--leak-ink); font-weight: 700; }
|
|
|
|
/* ── 이력 테이블: 카드형 헤더(테이블 위 캡션 스트립) ────────────────────────── */
|
|
h2.table-title { margin-bottom: 0; font-size: .9375rem; }
|
|
.table-title {
|
|
display: flex; align-items: center; gap: var(--sp-2);
|
|
background: var(--surface-2); border: 1px solid var(--line); border-bottom: none;
|
|
border-radius: var(--r-lg) var(--r-lg) 0 0; padding: 12px 16px;
|
|
font-size: .9375rem; font-weight: 700;
|
|
}
|
|
.table-title .count-pill {
|
|
margin-left: auto; font-size: .7rem; font-weight: 700; color: var(--ink-2);
|
|
background: var(--surface); border: 1px solid var(--line);
|
|
padding: 2px 10px; border-radius: var(--r-pill); font-variant-numeric: tabular-nums;
|
|
}
|
|
.table-title + .table-wrap,
|
|
.table-title + details.mobile-detail .table-wrap { border-radius: 0 0 var(--r-lg) var(--r-lg); }
|
|
.table-title + .table-wrap table,
|
|
.table-title + details.mobile-detail table { border-radius: 0 0 var(--r-lg) var(--r-lg); border-top: none; }
|
|
.table-title .h2-sub { margin-left: auto; font-size: .72rem; font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
|
|
/* ── 히어로 미세 조정: 큰 제목 음수 트래킹 ──────────────────────────────────── */
|
|
.status-hero h2 { letter-spacing: -.02em; }
|
|
.kpi .kpi-val, .card .val, .gauge-num b { letter-spacing: -.03em; }
|
|
|
|
/* ── 미세먼지(PM) 계통 색상·요소 (온습도 orange 와 구분되는 magenta/violet 계열) ──────
|
|
PMS7003 미세먼지 경보가 온습도 경보로 오독되지 않도록 별도 색 계통을 부여한다. PM 토큰은
|
|
app.css 에 없으므로 여기서 정의(dash-v2.css 는 app.css 뒤 로드 — 재정의 아님, additive). */
|
|
:root {
|
|
--pm: #d24fd6; /* 미세먼지 대표 액센트(도넛·칩·라인) */
|
|
--pm-ink: #eca3ea; /* 어두운 배경 위 텍스트용 밝은 변형 */
|
|
--pm-bg: rgba(210,79,214,.15);
|
|
--pm25: #e85fbf; /* PM2.5 계열(magenta-pink) */
|
|
--pm10: #a06bff; /* PM10 계열(violet) */
|
|
}
|
|
|
|
/* 유형 칩(미세먼지) — sc-type.metric(청록)·sc-type.leak(회색)과 구분 */
|
|
.sc-type.pm { background: var(--pm-bg); color: var(--pm-ink); }
|
|
|
|
/* 카드 상태(미세먼지 경보) — 상단 바 + 배경 그라디언트(온습도 alert 와 별개 색) */
|
|
.sensor-card.pm { border-color: rgba(210,79,214,.4); background: linear-gradient(180deg, var(--pm-bg), transparent 60%), var(--surface); }
|
|
.sensor-card.pm::before { background: var(--pm); }
|
|
|
|
/* 상태 라인(미세먼지 경보) */
|
|
.status-line.pmst { color: var(--pm-ink); }
|
|
.status-line.pmst .sdot { background: var(--pm); box-shadow: 0 0 8px var(--pm); animation: dash-pulse 1.2s ease-in-out infinite; }
|
|
|
|
/* 미세먼지 큰 수치 2열 — 온습도 vals 재사용, 색만 PM 계열. 위반 시 hot 글로우(magenta) */
|
|
.vals.pm-vals { margin-top: var(--sp-2); }
|
|
.vals .v.pm25 .n { color: var(--pm25); }
|
|
.vals .v.pm10 .n { color: var(--pm10); }
|
|
.pm-vals .v .n.hot { text-shadow: 0 0 14px rgba(210,79,214,.55); }
|
|
|
|
/* 상태 pill(미세먼지/결로) — 이력·측정 표의 상태 배지 */
|
|
.pill.high_pm25 { background: var(--pm-bg); color: var(--pm-ink); }
|
|
.pill.high_pm10 { background: var(--pm-bg); color: var(--pm-ink); }
|
|
.pill.condensation_risk { background: var(--warn-bg); color: var(--warn-ink); }
|
|
|
|
/* 추이 차트(미세먼지) — dashboard.php 인라인 <style> 에 PM 규칙이 없어 여기서 정의 */
|
|
.chart-svg-wrap .axis-pm { fill: var(--pm); }
|
|
.chart-svg-wrap .line-pm25 { stroke: var(--pm25); }
|
|
.chart-svg-wrap .line-pm10 { stroke: var(--pm10); }
|
|
.legend-swatch.pm25 { background: var(--pm25); }
|
|
.legend-swatch.pm10 { background: var(--pm10); }
|
|
|
|
/* 7일 경보 추이 라인(미세먼지) + 도넛 세그먼트·범례(미세먼지) */
|
|
.ov-trend .line-pm { stroke: var(--pm); }
|
|
.donut-ring circle.seg.cat-pm { stroke: var(--pm); }
|
|
.dl i.cat-pm { background: var(--pm); }
|
|
|
|
/* ── 접근성 ─────────────────────────────────────────────────────────────────── */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.sensor-card.leak { animation: none; }
|
|
.status-line .sdot { animation: none !important; }
|
|
.refresh-ring circle { animation: none; }
|
|
.kpi, .sensor-card, .leak-status-card, .alert-strip, .quick-tile,
|
|
.tabbar .tab-link, .top-actions a, .refresh-toggle { transition: none; }
|
|
.kpi:hover, .sensor-card:hover, .leak-status-card:hover, .alert-strip:hover { transform: none; }
|
|
}
|
|
@media (prefers-reduced-transparency: reduce) {
|
|
header { background: var(--header-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
}
|
|
|
|
/* ── 인쇄: 장식·모션 요소 제거(app.css 강제 라이트 계승) ─────────────────────── */
|
|
@media print {
|
|
.leak-wave, .refresh-ring, .spark24 { display: none !important; }
|
|
.sensor-card, .sensor-card.leak { animation: none; box-shadow: none; }
|
|
.status-line .sdot { animation: none; box-shadow: none; border: 1px solid currentColor; }
|
|
.vals .v .n { text-shadow: none; }
|
|
}
|