row) */ protected array $auditBeforeRows = []; /** * 모델 이벤트(콜백) 등록. * BaseModel 이 이미 $afterInsert 등 속성을 선언하므로(속성 충돌 방지), * 트레잇에서는 속성 대신 initialize()에서 런타임에 콜백을 덧붙인다. */ protected function initialize() { $this->allowCallbacks = true; $this->afterInsert[] = 'auditAfterInsert'; $this->beforeUpdate[] = 'auditBeforeUpdate'; $this->afterUpdate[] = 'auditAfterUpdate'; $this->beforeDelete[] = 'auditBeforeDelete'; $this->afterDelete[] = 'auditAfterDelete'; } /** 마스킹 대상 필드명 */ protected array $auditMaskFields = ['mb_passwd', 'mb_totp_secret', 'mb_session_token', 'mb_email', 'mb_phone']; /** * 이 필드들'만' 변경된 update는 활동 로그를 남기지 않는다. * (예: 로그인/세션 갱신 같은 시스템 내부 변경 — 노이즈 제거) * 모델에서 이 메서드를 override 해 사용. (프로퍼티로 두면 trait/class 비호환 오류) * * @return list */ protected function auditSkipUpdateOnlyFields(): array { return []; } private function auditMask(?array $row): ?array { if ($row === null) { return null; } foreach ($this->auditMaskFields as $f) { if (array_key_exists($f, $row) && (string) $row[$f] !== '') { $row[$f] = '***'; } } return $row; } /** find 콜백 재귀를 피하려고 쿼리빌더로 원본 행을 직접 조회 */ private function auditFetchRaw(int|string $id): ?array { try { $row = $this->db->table($this->table) ->where($this->primaryKey, $id) ->get() ->getRowArray(); return $row ?: null; } catch (\Throwable $e) { return null; } } public function auditAfterInsert(array $eventData): array { helper('audit'); $after = is_array($eventData['data'] ?? null) ? $eventData['data'] : null; audit_log('create', $this->table, (int) ($eventData['id'] ?? 0), null, $this->auditMask($after)); return $eventData; } public function auditBeforeUpdate(array $eventData): array { $this->auditBeforeRows = []; $ids = $eventData['id'] ?? null; if ($ids !== null) { foreach ((array) $ids as $one) { $row = $this->auditFetchRaw($one); if ($row !== null) { $this->auditBeforeRows[(string) $one] = $row; } } } return $eventData; } public function auditAfterUpdate(array $eventData): array { helper('audit'); $ids = $eventData['id'] ?? null; $changes = is_array($eventData['data'] ?? null) ? $eventData['data'] : []; // 지정 필드만 변경된 경우(로그인/세션 갱신 등) → 기록 생략 $skipOnly = $this->auditSkipUpdateOnlyFields(); if ($skipOnly !== [] && $changes !== []) { $changedKeys = array_values(array_diff(array_keys($changes), [$this->primaryKey])); if ($changedKeys !== [] && array_diff($changedKeys, $skipOnly) === []) { $this->auditBeforeRows = []; return $eventData; } } if ($ids !== null) { foreach ((array) $ids as $one) { $before = $this->auditBeforeRows[(string) $one] ?? null; $after = $before !== null ? array_merge($before, $changes) : $changes; audit_log('update', $this->table, (int) $one, $this->auditMask($before), $this->auditMask($after)); } } else { // 조건(where) 기반 일괄 수정: 대상 PK 미상 → 변경값만 기록 audit_log('update', $this->table, 0, null, $this->auditMask($changes)); } $this->auditBeforeRows = []; return $eventData; } public function auditBeforeDelete(array $eventData): array { $this->auditBeforeRows = []; $ids = $eventData['id'] ?? null; if ($ids !== null) { foreach ((array) $ids as $one) { $row = $this->auditFetchRaw($one); if ($row !== null) { $this->auditBeforeRows[(string) $one] = $row; } } } return $eventData; } public function auditAfterDelete(array $eventData): array { helper('audit'); $ids = $eventData['id'] ?? null; if ($ids !== null) { foreach ((array) $ids as $one) { $before = $this->auditBeforeRows[(string) $one] ?? null; audit_log('delete', $this->table, (int) $one, $this->auditMask($before), null); } } else { audit_log('delete', $this->table, 0, null, null); } $this->auditBeforeRows = []; return $eventData; } }