sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401); if (!Rbac::can((string)$this->sess->get('role'), 'edit_zones')) return Response::json(['error' => 'forbidden'], 403); if (!Csrf::check((string)$this->sess->get('csrf'), (string)$req->header('X-Csrf-Token'))) { return Response::json(['error' => 'csrf'], 403); } $b = $req->json(); $camera = trim((string)($b['camera_id'] ?? '')); $note = trim((string)($b['note'] ?? '')); if ($camera === '' || mb_strlen($camera) > 64 || $note === '' || mb_strlen($note) > self::MAX_NOTE) { return Response::json(['error' => 'invalid_action'], 400); } $now = time(); $takenAt = isset($b['taken_at']) && is_numeric($b['taken_at']) ? (int)$b['taken_at'] : $now; // 미래 시각·1년 초과 과거는 입력 실수: 전후 비교가 성립하지 않는다. if ($takenAt > $now || $takenAt < $now - 365 * 86400) { return Response::json(['error' => 'invalid_taken_at'], 400); } $id = $this->actions->insert([ 'camera_id' => $camera, 'note' => $note, 'taken_at' => $takenAt, 'created_by' => (string)$this->sess->get('username'), ]); return Response::json(['action_id' => $id], 201); } public function json(Request $req): Response { if ($this->sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401); $scope = $this->sess->get('site_scope'); $site = is_string($scope) ? $scope : null; $now = time(); $out = []; foreach ($this->actions->recent(self::LIST_LIMIT) as $a) { // 동일 길이의 전후 창으로만 비교해야 공정하다: 시행 후 경과가 W보다 짧으면 // 전 창도 그만큼으로 줄인다. 3일 미만은 표본이 아니라 소음 — "측정 중". $elapsed = $now - $a['taken_at']; $w = min(self::EFFECT_WINDOW_S, $elapsed); $effect = null; if ($w >= self::EFFECT_MIN_S) { $before = $this->events->countForCamera($a['camera_id'], (float)($a['taken_at'] - $w), (float)$a['taken_at'], $site); $after = $this->events->countForCamera($a['camera_id'], (float)$a['taken_at'], (float)($a['taken_at'] + $w), $site); $effect = ['before' => $before, 'after' => $after, 'window_days' => (int)round($w / 86400)]; } $out[] = $a + ['effect' => $effect]; } return Response::json(['actions' => $out], 200); } }