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 제외.
135 lines
6.2 KiB
HTML
135 lines
6.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>월간 보고서 - 2026-05</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; font-family: 'Noto Sans KR', sans-serif; background: #f0f4f8; color: #1f2937; }
|
|
header { background: #1a56db; color: #fff; padding: 16px 24px; display:flex; justify-content:space-between; align-items:center; gap:12px; }
|
|
header h1 { margin:0; font-size:1.15rem; }
|
|
header a { color:#fff; text-decoration:none; background:rgba(255,255,255,.15); padding:7px 12px; border-radius:6px; font-size:.85rem; }
|
|
.container { max-width: 1060px; margin: 24px auto; padding: 0 16px; }
|
|
.toolbar { background:#fff; border-radius:8px; padding:14px 16px; box-shadow:0 1px 4px rgba(0,0,0,.1); display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:18px; }
|
|
input, button, .btn { border:1px solid #cbd5e1; border-radius:6px; padding:8px 10px; font-size:.9rem; }
|
|
button, .btn { background:#1a56db; color:#fff; border-color:#1a56db; text-decoration:none; cursor:pointer; }
|
|
.toolbar label { display:flex; align-items:center; gap:6px; }
|
|
.grid { display:grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap:12px; margin-bottom:18px; }
|
|
.card { background:#fff; border-radius:8px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.1); }
|
|
.card span { color:#64748b; font-size:.78rem; display:block; margin-bottom:5px; }
|
|
.card strong { font-size:1.35rem; }
|
|
table { width:100%; border-collapse:collapse; background:#fff; border-radius:8px; overflow:hidden; box-shadow:0 1px 4px rgba(0,0,0,.1); margin-bottom:22px; }
|
|
th, td { padding:9px 12px; border-bottom:1px solid #e2e8f0; text-align:left; font-size:.84rem; }
|
|
th { background:#f8fafc; color:#64748b; }
|
|
h2 { font-size:1rem; color:#475569; margin:22px 0 10px; }
|
|
.status { font-weight:700; }
|
|
.table-wrap { overflow-x:auto; -webkit-overflow-scrolling:touch; margin-bottom:22px; border-radius:8px; }
|
|
.table-wrap table { margin-bottom:0; min-width:720px; }
|
|
@media print {
|
|
body { background:#fff; }
|
|
header, .toolbar { display:none; }
|
|
.container { margin:0; max-width:none; }
|
|
.card, table { box-shadow:none; }
|
|
}
|
|
@media (max-width:760px) { .grid { grid-template-columns: repeat(2, minmax(0,1fr)); } header { flex-direction:column; align-items:flex-start; } }
|
|
@media (max-width:520px) {
|
|
.container { margin:16px auto; padding:0 10px; }
|
|
header { padding:14px 16px; }
|
|
header h1 { font-size:1rem; }
|
|
.toolbar { display:grid; grid-template-columns:1fr; }
|
|
.toolbar label, .toolbar input, .toolbar button, .toolbar .btn { width:100%; }
|
|
.toolbar label { display:grid; gap:6px; }
|
|
.grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap:8px; }
|
|
.card { padding:12px; }
|
|
.card strong { font-size:1.05rem; }
|
|
}
|
|
@media (max-width:360px) { .grid { grid-template-columns:1fr; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>2026-05 월간 운영 보고서</h1>
|
|
<div>
|
|
<a href="#">대시보드</a>
|
|
</div>
|
|
</header>
|
|
<main class="container">
|
|
<form class="toolbar" method="GET">
|
|
<label>월 <input type="month" name="month" value="2026-05"></label>
|
|
<button type="submit">조회</button>
|
|
<a class="btn" href="#">CSV 다운로드</a>
|
|
<button type="button" onclick="window.print()">인쇄/PDF</button>
|
|
</form>
|
|
|
|
<section class="card" style="margin-bottom:18px">
|
|
<span>보고서 생성</span>
|
|
<strong style="font-size:1rem">2026-05-20 13:50:00</strong>
|
|
<p style="color:#64748b;font-size:.84rem;margin:8px 0 0">범위: 온습도 임계 경보, 장비 오프라인, SMS, 정상복귀/복구</p>
|
|
</section>
|
|
|
|
<section class="grid">
|
|
<div class="card"><span>고온 경보</span><strong>2건</strong></div>
|
|
<div class="card"><span>저온 경보</span><strong>0건</strong></div>
|
|
<div class="card"><span>고습 경보</span><strong>1건</strong></div>
|
|
<div class="card"><span>저습 경보</span><strong>0건</strong></div>
|
|
<div class="card"><span>정상복귀</span><strong>3건</strong></div>
|
|
<div class="card"><span>장비 오프라인</span><strong>1건</strong></div>
|
|
<div class="card"><span>SMS 성공/실패</span><strong>8/1</strong></div>
|
|
<div class="card"><span>측정 건수</span><strong>8,640건</strong></div>
|
|
</section>
|
|
|
|
<h2>센서별 요약</h2>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>센서</th><th>위치</th><th>임계 경보</th><th>평균 온도</th><th>평균 습도</th><th>최근 측정</th></tr></thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>센서 1</td>
|
|
<td>전산실 서버랙 A열 하단</td>
|
|
<td>2</td>
|
|
<td>24.1C</td>
|
|
<td>47.3%</td>
|
|
<td>2026-05-18 09:41:20</td>
|
|
</tr>
|
|
<tr>
|
|
<td>센서 2</td>
|
|
<td>전산실 상단 랙</td>
|
|
<td>1</td>
|
|
<td>25.4C</td>
|
|
<td>52.0%</td>
|
|
<td>2026-05-31 23:59:11</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h2>임계 경보 SMS 내역</h2>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>발생</th><th>센서</th><th>위치</th><th>경보 종류</th><th>측정값</th><th>정상복귀</th><th>SMS</th></tr></thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>2026-05-12 16:08:11</td>
|
|
<td>센서 2</td>
|
|
<td>전산실 상단 랙</td>
|
|
<td class="status">고습</td>
|
|
<td>25.1C / 72.4%</td>
|
|
<td>2026-05-12 16:38:30</td>
|
|
<td>성공</td>
|
|
</tr>
|
|
<tr>
|
|
<td>2026-05-18 09:13:42</td>
|
|
<td>센서 1</td>
|
|
<td>전산실 서버랙 A열 하단</td>
|
|
<td class="status">고온</td>
|
|
<td>31.2C / 41.0%</td>
|
|
<td>2026-05-18 09:45:20</td>
|
|
<td>성공</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
</body>
|
|
</html>
|