SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트). 전체 코드리뷰에서 도출된 보안 하드닝 10건 반영: - 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시) - 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php) - 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점) - CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php) - 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화 _backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
422 lines
19 KiB
HTML
422 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>관리자 대시보드 사용설명서</title>
|
|
<style>
|
|
:root {
|
|
--blue: #1d4ed8;
|
|
--blue-dark: #123c7c;
|
|
--green: #166534;
|
|
--red: #991b1b;
|
|
--amber: #92400e;
|
|
--ink: #172033;
|
|
--muted: #64748b;
|
|
--line: #d9e2ef;
|
|
--paper: #fff;
|
|
--page: #f4f7fb;
|
|
--soft-blue: #eff6ff;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html { scroll-behavior: smooth; }
|
|
body {
|
|
margin: 0;
|
|
font-family: "Noto Sans KR", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
|
|
color: var(--ink);
|
|
background: var(--page);
|
|
line-height: 1.68;
|
|
}
|
|
a { color: var(--blue); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
header {
|
|
background: var(--blue-dark);
|
|
color: #fff;
|
|
padding: 22px 24px;
|
|
border-bottom: 4px solid #0f766e;
|
|
}
|
|
.header-inner {
|
|
max-width: 1120px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 18px;
|
|
align-items: center;
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
font-size: clamp(1.35rem, 2.7vw, 2.2rem);
|
|
line-height: 1.25;
|
|
letter-spacing: 0;
|
|
}
|
|
.subtitle {
|
|
color: #dbeafe;
|
|
margin-top: 7px;
|
|
font-size: .94rem;
|
|
}
|
|
.print-btn {
|
|
border: 1px solid rgba(255,255,255,.32);
|
|
background: rgba(255,255,255,.13);
|
|
color: #fff;
|
|
border-radius: 8px;
|
|
padding: 9px 12px;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
main {
|
|
max-width: 1120px;
|
|
margin: 0 auto;
|
|
padding: 24px 16px 56px;
|
|
}
|
|
.summary {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.1fr) minmax(320px, .9fr);
|
|
gap: 18px;
|
|
align-items: start;
|
|
margin-bottom: 18px;
|
|
}
|
|
.panel {
|
|
background: var(--paper);
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
}
|
|
.panel h2, section h2 {
|
|
margin: 0 0 12px;
|
|
font-size: 1.14rem;
|
|
letter-spacing: 0;
|
|
}
|
|
.panel p, section p { margin: 0 0 10px; color: #334155; }
|
|
.toc {
|
|
display: flex;
|
|
gap: 8px;
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
padding: 3px 0 18px;
|
|
}
|
|
.toc a {
|
|
flex: 0 0 auto;
|
|
border: 1px solid #bfdbfe;
|
|
background: var(--soft-blue);
|
|
color: var(--blue);
|
|
border-radius: 999px;
|
|
padding: 8px 12px;
|
|
font-size: .84rem;
|
|
font-weight: 800;
|
|
}
|
|
section {
|
|
background: var(--paper);
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
padding: 22px;
|
|
margin: 16px 0;
|
|
}
|
|
h3 {
|
|
margin: 18px 0 8px;
|
|
color: #334155;
|
|
font-size: 1rem;
|
|
letter-spacing: 0;
|
|
}
|
|
.shot {
|
|
margin: 14px 0;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
background: #fff;
|
|
}
|
|
.shot img {
|
|
width: 100%;
|
|
display: block;
|
|
}
|
|
.shot figcaption {
|
|
padding: 9px 12px;
|
|
border-top: 1px solid var(--line);
|
|
color: var(--muted);
|
|
font-size: .84rem;
|
|
background: #fbfdff;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 12px;
|
|
}
|
|
.tile {
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
background: #fbfdff;
|
|
padding: 14px;
|
|
}
|
|
.tile strong { display: block; margin-bottom: 5px; }
|
|
.tile span { color: var(--muted); font-size: .9rem; }
|
|
.state {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
border-radius: 999px;
|
|
padding: 4px 9px;
|
|
font-size: .78rem;
|
|
font-weight: 900;
|
|
}
|
|
.state.ok { background: #dcfce7; color: var(--green); }
|
|
.state.warn { background: #fef3c7; color: var(--amber); }
|
|
.state.danger { background: #fee2e2; color: var(--red); }
|
|
.notice {
|
|
border-left: 5px solid var(--blue);
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
background: var(--soft-blue);
|
|
color: #1e3a8a;
|
|
margin: 12px 0;
|
|
}
|
|
.notice.warn { border-left-color: #f59e0b; background: #fffbeb; color: #713f12; }
|
|
.notice.danger { border-left-color: #ef4444; background: #fef2f2; color: #7f1d1d; }
|
|
.step-list {
|
|
display: grid;
|
|
gap: 10px;
|
|
counter-reset: step;
|
|
}
|
|
.step {
|
|
display: grid;
|
|
grid-template-columns: 34px minmax(0, 1fr);
|
|
gap: 12px;
|
|
align-items: start;
|
|
}
|
|
.step::before {
|
|
counter-increment: step;
|
|
content: counter(step);
|
|
width: 34px;
|
|
height: 34px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 8px;
|
|
background: var(--blue);
|
|
color: #fff;
|
|
font-weight: 900;
|
|
}
|
|
.step p { margin: 0; }
|
|
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
margin: 12px 0;
|
|
}
|
|
th, td {
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid var(--line);
|
|
text-align: left;
|
|
vertical-align: top;
|
|
font-size: .9rem;
|
|
}
|
|
th { background: #f8fafc; color: #475569; }
|
|
tr:last-child td { border-bottom: 0; }
|
|
code {
|
|
font-family: Consolas, "Courier New", monospace;
|
|
background: #eef2ff;
|
|
color: #312e81;
|
|
border-radius: 5px;
|
|
padding: 1px 5px;
|
|
font-size: .88em;
|
|
}
|
|
.daily {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 10px;
|
|
}
|
|
.check {
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
background: #fbfdff;
|
|
}
|
|
.check strong { display: block; margin-bottom: 4px; }
|
|
.check span { color: var(--muted); font-size: .88rem; }
|
|
|
|
@media (max-width: 860px) {
|
|
.header-inner { align-items: flex-start; flex-direction: column; }
|
|
.summary { grid-template-columns: 1fr; }
|
|
.grid { grid-template-columns: 1fr; }
|
|
.daily { grid-template-columns: 1fr; }
|
|
}
|
|
@media (max-width: 560px) {
|
|
header { padding: 18px 16px; }
|
|
main { padding: 16px 10px 42px; }
|
|
section, .panel { padding: 16px; }
|
|
th, td { font-size: .82rem; padding: 8px; }
|
|
.step { grid-template-columns: 30px minmax(0, 1fr); }
|
|
.step::before { width: 30px; height: 30px; }
|
|
}
|
|
@media print {
|
|
body { background: #fff; }
|
|
header, .toc, .print-btn { display: none; }
|
|
main { max-width: none; padding: 0; }
|
|
section, .panel { break-inside: avoid; border-color: #cbd5e1; }
|
|
.shot { break-inside: avoid; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="header-inner">
|
|
<div>
|
|
<h1>관리자 대시보드 사용설명서</h1>
|
|
<div class="subtitle">SHT30 온습도 모니터링, 임계 경보 SMS, 1번·2번 센서 상태 확인용</div>
|
|
</div>
|
|
<button class="print-btn" type="button" onclick="window.print()">인쇄 / PDF</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<div class="summary">
|
|
<article class="panel">
|
|
<h2>문서 목적</h2>
|
|
<p>관리자가 대시보드에 접속했을 때 가장 먼저 무엇을 봐야 하는지, 임계 경보가 발생했을 때 어떤 순서로 확인해야 하는지, 1번/2번 센서의 온습도 상태를 어떻게 판독해야 하는지 설명한다.</p>
|
|
<p>이 문서의 캡처는 운영 대시보드와 동일한 구성으로 만든 예시 화면이다. 실제 숫자, 시간, 측정값은 운영 DB 데이터에 따라 달라진다.</p>
|
|
</article>
|
|
<article class="panel">
|
|
<h2>관리자가 매일 확인할 것</h2>
|
|
<div class="daily">
|
|
<div class="check"><strong>임계 경보</strong><span>0건인지 확인. 1건 이상이면 해당 센서의 온습도와 임계 초과 항목을 확인.</span></div>
|
|
<div class="check"><strong>오프라인</strong><span>0대인지 확인. 오프라인이면 Pi 전원/네트워크/API 점검.</span></div>
|
|
<div class="check"><strong>센서 온습도</strong><span>1번·2번 센서의 온도/습도와 최신 측정 시간이 갱신되는지 확인.</span></div>
|
|
<div class="check"><strong>운영 자가진단</strong><span>API 키, DB 테이블, SMS 실패 경고를 확인.</span></div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<nav class="toc" aria-label="문서 목차">
|
|
<a href="#login">접속</a>
|
|
<a href="#overview">첫 화면</a>
|
|
<a href="#sensors">센서 상태</a>
|
|
<a href="#alerts">임계 경보</a>
|
|
<a href="#logs">추이/로그</a>
|
|
<a href="#mobile">모바일</a>
|
|
<a href="#trouble">이상 대처</a>
|
|
</nav>
|
|
|
|
<section id="login">
|
|
<h2>1. 접속과 로그인</h2>
|
|
<div class="step-list">
|
|
<div class="step"><p>브라우저에서 운영 URL의 <code>dashboard.php</code>로 접속한다. 반드시 HTTPS 주소를 사용한다.</p></div>
|
|
<div class="step"><p>관리자 계정으로 로그인한다. 공용 PC에서는 저장된 비밀번호를 사용하지 않는다.</p></div>
|
|
<div class="step"><p>작업 완료 후 우측 상단 <code>로그아웃</code>을 눌러 세션을 종료한다.</p></div>
|
|
</div>
|
|
<div class="notice warn">로그인이 반복해서 풀리면 서버 시간, 세션 저장 경로, 브라우저 쿠키 차단 여부를 확인한다.</div>
|
|
</section>
|
|
|
|
<section id="overview">
|
|
<h2>2. 첫 화면에서 보는 순서</h2>
|
|
<figure class="shot">
|
|
<img src="captures/admin_dashboard_overview.png" alt="관리자 대시보드 데스크톱 전체 화면 캡처">
|
|
<figcaption>데스크톱 대시보드 예시 캡처. 상단 상태바, 온습도 추이, 자가진단, 요약 카드, 센서 카드 순서로 확인한다. ※ 캡처 이미지는 SHT30 화면으로 재촬영 필요.</figcaption>
|
|
</figure>
|
|
<div class="grid">
|
|
<div class="tile"><strong>상단 상태바</strong><span><span class="state ok">정상</span>이면 운영 경고 없음. <span class="state warn">주의</span>나 <span class="state danger">위험</span>이면 바로 하단 세부 항목을 확인한다.</span></div>
|
|
<div class="tile"><strong>요약 카드</strong><span>등록 센서, 임계 경보, 오프라인, 30일 임계 SMS, SMS 실패를 빠르게 확인한다.</span></div>
|
|
<div class="tile"><strong>운영 자가진단</strong><span>API 키, DB 테이블, sensor_metric 저장, SMS 실패 등 운영 준비 상태를 보여준다.</span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="sensors">
|
|
<h2>3. 센서 상태 판독</h2>
|
|
<p>센서 카드는 현장의 장비별 온습도 상태를 보여준다. 정상/임계 경보/오프라인 우선순위로 색상이 바뀐다.</p>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr><th>센서</th><th>관리자가 확인할 내용</th><th>주의 기준</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>1번 센서</td>
|
|
<td>SHT30 온습도 장비. 온도, 습도, 측정 시간이 표시된다.</td>
|
|
<td>온습도 값이 사라짐, 측정 시간이 갱신되지 않음, 임계 초과, 오프라인 표시.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>2번 센서</td>
|
|
<td>SHT30 온습도 장비. 온도, 습도, 측정 시간이 표시된다.</td>
|
|
<td>온습도 값이 사라짐, 측정 시간이 갱신되지 않음, 임계 초과, 오프라인 표시.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="notice">센서가 정상이라면 카드 안에 <code>온도 24.3C</code>, <code>습도 48.1%</code> 같은 최신값이 표시된다. 임계(고온 30C / 저온 10C / 고습 70% / 저습 20%)를 넘으면 경보 SMS가 발송된다.</div>
|
|
</section>
|
|
|
|
<section id="alerts">
|
|
<h2>4. 임계 경보 확인 순서</h2>
|
|
<figure class="shot">
|
|
<img src="captures/admin_dashboard_overview.png" alt="온습도 추이와 임계 경보 영역 캡처">
|
|
<figcaption>온습도 추이 영역 예시 캡처. 임계(고온/저온/고습/저습)를 넘으면 경보 SMS가 발송되고 추이 차트와 임계 SMS 이력에 표시된다. ※ 캡처 이미지는 SHT30 화면으로 재촬영 필요.</figcaption>
|
|
</figure>
|
|
<div class="step-list">
|
|
<div class="step"><p><strong>임계 경보</strong> 건수가 1건 이상이면 해당 센서의 온도/습도와 초과 항목을 먼저 확인한다.</p></div>
|
|
<div class="step"><p>온습도 추이 차트에서 값이 임계선을 넘은 시점과 지속 시간을 확인한다.</p></div>
|
|
<div class="step"><p>임계 경보 SMS 이력에서 고온/저온/고습/저습 중 어떤 종류의 경보가 발송됐는지 확인한다.</p></div>
|
|
<div class="step"><p>값이 임계 안으로 돌아오면 <code>정상복귀</code> SMS가 발송된다. 복귀 여부를 이력에서 확인한다.</p></div>
|
|
</div>
|
|
<div class="notice danger">임계 경보가 반복되면 서버실 공조/환기 상태와 센서 설치 위치를 점검한다. 오프라인 경보가 함께 뜨면 Pi 전원/네트워크부터 확인한다.</div>
|
|
</section>
|
|
|
|
<section id="logs">
|
|
<h2>5. 온습도 추이, 로그, SMS, 월간 보고서</h2>
|
|
<div class="grid">
|
|
<div class="tile"><strong>온습도 추이 차트</strong><span>센서별 온도/습도 시계열과 임계선을 확인한다. 임계 초과 구간이 강조된다.</span></div>
|
|
<div class="tile"><strong>최근 이벤트 로그</strong><span>정기보고, 임계 경보, 정상복귀, 오프라인, 장비 시작 이벤트를 시간순으로 확인한다.</span></div>
|
|
<div class="tile"><strong>SMS 발송 이력</strong><span>임계 경보 문자 성공/실패, 수신자, 결과 메시지를 확인한다.</span></div>
|
|
</div>
|
|
<h3>월간 보고서</h3>
|
|
<p>상단 <code>월간 보고서</code>에서 월별 종류별 임계 경보 SMS 건수, 정상복귀/오프라인/복구 건수, 평균/최저/최고 온습도, CSV 다운로드를 확인한다. 대외 제출용은 인쇄/PDF 저장으로 보관한다.</p>
|
|
</section>
|
|
|
|
<section id="mobile">
|
|
<h2>6. 모바일에서 빠르게 확인하기</h2>
|
|
<figure class="shot" style="max-width:430px">
|
|
<img src="captures/admin_dashboard_mobile.png" alt="관리자 대시보드 모바일 화면 캡처">
|
|
<figcaption>모바일 예시 캡처. 상단 요약 카드와 빠른 링크를 먼저 보고 필요한 섹션으로 이동한다. ※ 캡처 이미지는 SHT30 화면으로 재촬영 필요.</figcaption>
|
|
</figure>
|
|
<p>모바일에서는 표와 상세 로그가 접히거나 가로 스크롤로 보일 수 있다. 긴급 상황에서는 <code>임계 경보</code>, <code>오프라인</code>, <code>평균 온도</code>, <code>평균 습도</code> 네 항목을 먼저 확인한다.</p>
|
|
</section>
|
|
|
|
<section id="trouble">
|
|
<h2>7. 이상 상태별 조치</h2>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr><th>화면 표시</th><th>의미</th><th>관리자 조치</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><span class="state danger">임계 경보 1건 이상</span></td>
|
|
<td>온도 또는 습도가 임계(고온 30C / 저온 10C / 고습 70% / 저습 20%)를 넘었다.</td>
|
|
<td>온습도 추이와 초과 항목 확인 → 서버실 공조/환기 점검 → 정상복귀 SMS 수신 확인.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="state warn">오프라인 1대 이상</span></td>
|
|
<td>라즈베리파이가 heartbeat 시간 안에 보고하지 않았다.</td>
|
|
<td>전원, 네트워크, systemd 서비스, API URL/API_KEY 확인.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="state warn">센서 온습도 미표시</span></td>
|
|
<td>SHT30 데이터가 저장되지 않았거나 측정이 실패했다.</td>
|
|
<td><code>sensor_metric</code> 테이블, <code>sht30-sensor</code> 서비스, I2C 주소 <code>0x44/0x45</code> 확인.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="state warn">SMS 실패</span></td>
|
|
<td>Cafe24 SMS 연동 또는 수신자 설정 문제가 있다.</td>
|
|
<td>SMS 계정, 잔액, 발신번호, 수신자 번호, 서버 외부 통신 가능 여부 확인.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="state danger">API 키 경고</span></td>
|
|
<td>운영 키가 기본값이거나 Pi와 서버 키가 다를 수 있다.</td>
|
|
<td>서버 <code>config.local.php</code>와 Pi 환경파일의 <code>LEAK_API_KEY</code>를 동일하게 맞춘다.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>
|