POSA_LEAKSMS/php/setup_wizard.php
cw2007 f7fbeacbdd feat: PM 급상승을 화재의심으로 전환 + SMS 단문화 + 센서고착 검출 비활성
서버실/항온항습: 여과·양압이라 평상 PM≈0 → PM 급상승을 화재 조기징후로 본다.
- 임계 하향: METRIC_PM25_HIGH 75→35, METRIC_PM10_HIGH 150→75 (연기 조기감지).
- SMS 태그 [미세먼지경보] → [화재의심]. 대시보드 라벨 통일(경보=화재의심, PM 측정=화재감시).
  metric_status(high_pm25/10) 코드·값·판정 로직 불변 — 표시단만 변경.
- SMS 단문화: 전 본문을 EUC-KR 90B 이내로 축약(관리주체 제거, 날짜 m-d H:i, 태그+위치 1줄).
  실측 45~70B ≤ 90B → 게이트웨이(Cafe24 smsType=S) 단문 절단 해소.
- 센서고착(live-but-stuck) 검출 기본 비활성(STUCK_DETECT_ENABLED=false):
  항온항습 문서고는 온습도 원래 평탄 → 평탄값을 고착으로 오탐하던 [센서고착] 문자 차단.
- 테스트: 태그·기본임계 단언 갱신. 회귀 스위트 PASS(9 run/0 fail), php -l 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Dj4n17ZvGAhga6D4pNRjn
2026-07-19 23:34:57 +09:00

118 lines
5.2 KiB
PHP

<?php
// =============================================================================
// setup_wizard.php - 설치/운영 점검 화면 (v2606, SHT30 온습도)
// =============================================================================
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/sms_send.php';
require_once __DIR__ . '/ops_checks.php';
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
if (empty($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
header('Location: login.php');
exit;
}
admin_enforce_session_timeout(); // 유휴 + 절대 수명 타임아웃(ops_checks → admin_security 경유 로드)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$checks = collect_ops_checks();
$sms_test_result = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'send_test_sms') {
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
http_response_code(400);
echo 'Invalid request';
exit;
}
$message = "[모니터링 테스트]\n"
. "설치/운영 점검 문자\n"
. date('m-d H:i');
$results = [];
foreach (SMS_RECIPIENTS as $receiver) {
$digits = preg_replace('/\D+/', '', $receiver);
$results[] = ['receiver' => $digits, 'result' => send_sms_cafe24($digits, $message)];
}
$sms_test_result = $results;
}
$failed_count = ops_failed_count($checks);
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>설치 점검 - 온습도 모니터링 <?= htmlspecialchars(APP_VERSION) ?></title>
<link rel="stylesheet" href="assets/app.css">
<style>
/* 설치 점검 전용 잔여 스타일 — 값은 app.css 토큰(var)만 사용 */
.wiz-summary { margin-bottom: var(--sp-5); }
.wiz-summary strong { font-size: 1.25rem; }
.check-list { display: grid; gap: var(--sp-3); margin-bottom: var(--sp-5); }
/* 점검 카드 = .panel + 좌측레일(도형). 상태 텍스트는 .check-ok/.check-fail(글리프+색) */
.check-list .panel { border-left: 6px solid var(--normal); }
.check-list .panel.is-fail { border-left-color: var(--leak); background: var(--leak-bg); }
.wiz-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin: var(--sp-5) 0; }
.wiz-actions button {
display: inline-block; background: var(--primary); color: var(--on-primary);
border: 0; border-radius: var(--r-sm); padding: 10px 16px;
font-size: .8125rem; font-weight: 700; font-family: var(--font); cursor: pointer;
}
.wiz-actions button:hover { background: var(--primary-600); }
@media print { .wiz-actions { display: none; } }
</style>
</head>
<body>
<header>
<h1>설치/운영 점검 <?= htmlspecialchars(APP_VERSION) ?></h1>
<div class="top-actions">
<a href="dashboard.php">대시보드</a>
<a href="security_evidence.php">보안 증적</a>
</div>
</header>
<main class="container">
<section class="panel wiz-summary">
<strong class="<?= $failed_count === 0 ? 'check-ok' : 'check-fail' ?>"><?= $failed_count === 0 ? '전체 점검 통과' : "확인 필요 {$failed_count}건" ?></strong>
<p class="caption">운영 반영 전 이 화면에서 설정, DB, 권한, SMS를 확인합니다.</p>
</section>
<div class="check-list">
<?php foreach ($checks as $check): ?>
<section class="panel<?= $check['ok'] ? '' : ' is-fail' ?>">
<h2><?= htmlspecialchars($check['category']) ?> · <?= htmlspecialchars($check['label']) ?> · <span class="<?= $check['ok'] ? 'check-ok' : 'check-fail' ?>"><?= $check['ok'] ? '정상' : '확인 필요' ?></span></h2>
<p class="caption"><?= htmlspecialchars($check['detail']) ?></p>
<?php if (!$check['ok'] && $check['fix'] !== ''): ?>
<p class="caption">조치: <?= htmlspecialchars($check['fix']) ?></p>
<?php endif; ?>
<?php if (($check['evidence'] ?? '') !== ''): ?>
<p class="caption">증적: <?= htmlspecialchars($check['evidence']) ?></p>
<?php endif; ?>
</section>
<?php endforeach; ?>
</div>
<form method="POST" class="wiz-actions">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<button type="submit" name="action" value="send_test_sms">테스트 SMS 발송</button>
</form>
<?php if ($sms_test_result !== null): ?>
<section class="panel">
<h2>테스트 SMS 결과</h2>
<?php foreach ($sms_test_result as $row): ?>
<p class="caption"><?= htmlspecialchars(mask_phone($row['receiver'])) ?> · <span class="<?= $row['result']['success'] ? 'check-ok' : 'check-fail' ?>"><?= $row['result']['success'] ? '성공' : '실패' ?></span> · <?= htmlspecialchars($row['result']['message']) ?></p>
<?php endforeach; ?>
</section>
<?php endif; ?>
</main>
</body>
</html>