Audit 모듈
이 문서는 검토 중입니다. 내용이 변경될 수 있습니다.
모델 변경 이력을 자동으로 기록하는 감사 로그 Core 모듈입니다.
개요
Audit 모듈은 모델의 CRUD 작업을 자동으로 추적하여 감사 로그를 생성합니다. Auditable Trait을 적용한 모델은 생성, 수정, 삭제 시 자동으로 변경 이력이 기록됩니다.
┌─────────────────────────────────────────────────────────────┐
│ Audit 모듈 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Model (Auditable Trait) │ │
│ │ │ │
│ │ create() ──┐ │ │
│ │ update() ──┼──► AuditObserver ──► AuditLog │ │
│ │ delete() ──┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 기록 정보: │
│ • 이벤트 (created, updated, deleted) │
│ • 변경 전/후 값 │
│ • 실행 사용자, 테넌트, IP, URL │
└─────────────────────────────────────────────────────────────┘
핵심 컴포넌트
| 컴포넌트 | 역할 |
|---|---|
Auditable | 모델에 적용하는 Trait |
AuditableInterface | Auditable 모델 계약 |
AuditObserver | 모델 이벤트 감지 및 로그 생성 |
AuditLog | 감사 로그 저장 모델 |
Auditable Trait 적용
기본 사용법
<?php
namespace App\Models;
use App\Core\Base\Audit\Contracts\AuditableInterface;
use App\Core\Base\Audit\Traits\Auditable;
use Illuminate\Database\Eloquent\Model;
class Product extends Model implements AuditableInterface
{
use Auditable;
protected $fillable = [
'name',
'price',
'description',
];
}
적용 즉시 동작
// 생성 → created 이벤트 기록
$product = Product::create([
'name' => 'New Product',
'price' => 10000,
]);
// 수정 → updated 이벤트 기록 (변경된 필드만)
$product->update(['price' => 15000]);
// 삭제 → deleted 이벤트 기록
$product->delete();
기록되는 정보
| 필드 | 설명 | 예시 |
|---|---|---|
auditable_type | 모델 클래스명 | App\Models\Product |
auditable_id | 모델 ID | 123 |
user_id | 실행한 사용자 ID | 1 |
tenant_id | 테넌트 ID | 5 |
event | 이벤트 유형 | created, updated, deleted |
old_values | 변경 전 값 (JSON) | {"price": 10000} |
new_values | 변경 후 값 (JSON) | {"price": 15000} |
ip_address | 클라이언트 IP | 192.168.1.1 |
user_agent | 브라우저 정보 | Mozilla/5.0... |
url | 요청 URL | /admin/products/123 |
metadata | 추가 메타데이터 | {"order_number": "ORD-001"} |
created_at | 기록 시간 | 2024-01-15 10:30:00 |
커스터마이징
특정 필드만 기록
class Order extends Model implements AuditableInterface
{
use Auditable;
/**
* 이 필드들만 감사 로그에 기록
*/
protected array $auditInclude = [
'status',
'total_amount',
'payment_status',
];
}
민감 정보 제외
class User extends Model implements AuditableInterface
{
use Auditable;
/**
* 이 필드들은 감사 로그에서 제외
*/
protected array $auditExclude = [
'password',
'remember_token',
'two_factor_secret',
'api_key',
];
}
특정 이벤트만 기록
class Document extends Model implements AuditableInterface
{
use Auditable;
/**
* 이 이벤트들만 기록 (삭제는 기록 안 함)
*/
protected array $auditEvents = [
'created',
'updated',
];
}
추가 메타데이터 포함
class Order extends Model implements AuditableInterface
{
use Auditable;
/**
* 감사 로그에 추가할 메타데이터
*/
public function getAuditMetadata(): array
{
return [
'order_number' => $this->order_number,
'customer_id' => $this->customer_id,
'customer_name' => $this->customer?->name,
];
}
}
조건부 활성화/비활성화
class Message extends Model implements AuditableInterface
{
use Auditable;
/**
* 감사 로그 활성화 여부
*/
public function isAuditEnabled(): bool
{
// Draft 상태에서는 기록 안 함
if ($this->status === 'draft') {
return false;
}
return config('core.audit.enabled', true);
}
}
일시 비활성화
특정 인스턴스만 비활성화
// 이 저장에서만 감사 로그 비활성화
$user->withoutAudit()->save();
// 체인 가능
$user->withoutAudit()->update(['name' => 'New Name']);
전역 비활성화 (마이그레이션, 시더)
// 콜백 내에서 모든 Auditable 모델의 로깅 비활성화
User::withoutAuditing(function () {
// 대량 데이터 삽입
User::factory()->count(1000)->create();
});
// 마이그레이션에서 사용
public function run(): void
{
Product::withoutAuditing(function () {
Product::insert([
['name' => 'Product 1', 'price' => 1000],
['name' => 'Product 2', 'price' => 2000],
// ...
]);
});
}
감사 로그 조회
AuditLog 모델 스코프
use App\Core\Base\Audit\Models\AuditLog;
// 특정 모델의 로그
$logs = AuditLog::forModel(Product::class)->get();
// 특정 이벤트의 로그
$logs = AuditLog::event('deleted')->get();
// 특정 사용자의 로그
$logs = AuditLog::byUser($userId)->get();
// 특정 테넌트의 로그
$logs = AuditLog::byTenant($tenantId)->get();
// 날짜 범위 조회
$logs = AuditLog::between('2024-01-01', '2024-01-31')->get();
// 조합 사용
$logs = AuditLog::forModel(User::class)
->event('updated')
->byTenant($tenantId)
->between($startDate, $endDate)
->latest()
->paginate(20);
모델에서 로그 조회
$product = Product::find(1);
// 해당 모델의 모든 감사 로그 (최신순)
$auditLogs = $product->auditLogs;
// 페이지네이션
$auditLogs = $product->auditLogs()->paginate(10);
// 특정 이벤트만
$deletedLogs = $product->auditLogs()
->where('event', 'deleted')
->get();
변경 내역 확인
$auditLog = AuditLog::find(1);
// 모든 변경 사항 (old와 new가 다른 필드만)
$changes = $auditLog->changes;
// [
// 'price' => ['old' => 10000, 'new' => 15000],
// 'name' => ['old' => 'Old Name', 'new' => 'New Name'],
// ]
// 개별 값
$oldValues = $auditLog->old_values;
$newValues = $auditLog->new_values;
관계 조회
$auditLog = AuditLog::find(1);
// 변경을 수행한 사용자
$user = $auditLog->user;
// 테넌트
$tenant = $auditLog->tenant;
// 감사 대상 모델
$product = $auditLog->auditable;
설정 (config/core.php)
'audit' => [
// 감사 로그 활성화
'enabled' => env('CORE_AUDIT_ENABLED', true),
// 저장 테이블명
'table' => 'audit_logs',
// 기록할 이벤트 (기본값)
'events' => ['created', 'updated', 'deleted'],
// 전역 제외 속성 (모든 모델에 적용)
'exclude_attributes' => [
'password',
'remember_token',
'two_factor_secret',
],
],
마이그레이션
// database/migrations/xxxx_create_audit_logs_table.php
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->morphs('auditable'); // auditable_type, auditable_id
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('event'); // created, updated, deleted, restored, force_deleted
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('url')->nullable();
$table->json('metadata')->nullable();
$table->timestamp('created_at');
// 인덱스
$table->index(['auditable_type', 'auditable_id']);
$table->index('user_id');
$table->index('tenant_id');
$table->index('event');
$table->index('created_at');
});
지원 이벤트
| 이벤트 | 설명 | old_values | new_values |
|---|---|---|---|
created | 모델 생성 | - | ✓ |
updated | 모델 수정 | ✓ | ✓ |
deleted | 소프트/하드 삭제 | ✓ | - |
restored | 소프트 삭제 복원 | - | ✓ |
force_deleted | 영구 삭제 | ✓ | - |
Filament 통합
Filament Admin에서 감사 로그 표시:
// app/Filament/Resources/ProductResource.php
use App\Core\Base\Audit\Models\AuditLog;
class ProductResource extends Resource
{
public static function getRelations(): array
{
return [
AuditLogsRelationManager::class,
];
}
}
// app/Filament/Resources/ProductResource/RelationManagers/AuditLogsRelationManager.php
class AuditLogsRelationManager extends RelationManager
{
protected static string $relationship = 'auditLogs';
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('event')
->badge()
->color(fn (string $state): string => match ($state) {
'created' => 'success',
'updated' => 'warning',
'deleted' => 'danger',
default => 'gray',
}),
Tables\Columns\TextColumn::make('user.name')
->label('사용자'),
Tables\Columns\TextColumn::make('created_at')
->label('시간')
->dateTime('Y-m-d H:i:s'),
])
->defaultSort('created_at', 'desc');
}
}
적용 기준
| 적용 필수 | 적용 권장 | 적용 불필요 |
|---|---|---|
| User, 권한 관련 | 비즈니스 엔티티 | 캐시 테이블 |
| Tenant, Organization | Order, Product | 로그 테이블 |
| 민감 데이터 | 일반 CRUD | Session, Job |
| 금융 데이터 | 문서, 게시물 | Pivot 테이블 |
테스트 작성
감사 로그 생성 테스트
use App\Core\Base\Audit\Models\AuditLog;
use App\Models\Product;
use App\Models\User;
test('모델 생성 시 감사 로그가 기록된다', function () {
$user = User::factory()->create();
$this->actingAs($user);
$product = Product::create([
'name' => 'Test Product',
'price' => 10000,
]);
$auditLog = AuditLog::where('auditable_type', Product::class)
->where('auditable_id', $product->id)
->where('event', 'created')
->first();
expect($auditLog)->not->toBeNull()
->and($auditLog->user_id)->toBe($user->id)
->and($auditLog->new_values['name'])->toBe('Test Product');
});
test('모델 수정 시 변경된 필드만 기록된다', function () {
$user = User::factory()->create();
$this->actingAs($user);
$product = Product::create([
'name' => 'Test Product',
'price' => 10000,
]);
$product->update(['price' => 15000]);
$auditLog = AuditLog::where('auditable_type', Product::class)
->where('auditable_id', $product->id)
->where('event', 'updated')
->first();
expect($auditLog)->not->toBeNull()
->and($auditLog->old_values['price'])->toBe(10000)
->and($auditLog->new_values['price'])->toBe(15000);
});
민감 정보 제외 테스트
test('민감 정보는 감사 로그에서 제외된다', function () {
$user = User::factory()->create([
'password' => bcrypt('password123'),
]);
$auditLog = AuditLog::where('auditable_type', User::class)
->where('auditable_id', $user->id)
->where('event', 'created')
->first();
expect($auditLog->new_values)->not->toHaveKey('password')
->and($auditLog->new_values)->not->toHaveKey('remember_token');
});
일시 비활성화 테스트
test('withoutAudit()로 감사 로그를 비활성화할 수 있다', function () {
$product = Product::create(['name' => 'Test', 'price' => 1000]);
$beforeCount = AuditLog::count();
$product->withoutAudit()->update(['price' => 2000]);
expect(AuditLog::count())->toBe($beforeCount);
});
test('withoutAuditing()으로 전역 비활성화할 수 있다', function () {
$beforeCount = AuditLog::count();
Product::withoutAuditing(function () {
Product::factory()->count(5)->create();
});
expect(AuditLog::count())->toBe($beforeCount);
});
보존 정책
대량의 감사 로그 관리를 위한 정리 전략:
Artisan 명령어
// app/Console/Commands/PruneAuditLogs.php
class PruneAuditLogs extends Command
{
protected $signature = 'audit:prune {--days=90}';
public function handle()
{
$days = $this->option('days');
$cutoff = now()->subDays($days);
$deleted = AuditLog::where('created_at', '<', $cutoff)->delete();
$this->info("{$deleted}개의 오래된 감사 로그가 삭제되었습니다.");
}
}
스케줄 등록
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// 매일 새벽 2시에 90일 이상 된 로그 삭제
$schedule->command('audit:prune --days=90')
->dailyAt('02:00');
}
모범 사례
1. 모든 비즈니스 모델에 적용
// 모든 비즈니스 중요 모델에 Auditable 적용
class Customer extends Model implements AuditableInterface
{
use Auditable;
}
class Order extends Model implements AuditableInterface
{
use Auditable;
}
class Payment extends Model implements AuditableInterface
{
use Auditable;
}
2. 민감 정보 철저히 제외
class User extends Model implements AuditableInterface
{
use Auditable;
protected array $auditExclude = [
'password',
'remember_token',
'two_factor_secret',
'api_key',
'secret_question_answer',
];
}
3. 메타데이터로 컨텍스트 보강
class Order extends Model implements AuditableInterface
{
use Auditable;
public function getAuditMetadata(): array
{
return [
'order_number' => $this->order_number,
'customer_email' => $this->customer?->email,
'total_items' => $this->items()->count(),
];
}
}
4. 대량 작업 시 비활성화
// 시더, 마이그레이션, 배치 작업에서
Product::withoutAuditing(function () {
Product::insert($bulkData);
});
감사로그 계층 가시성 (Hierarchical Visibility)
관리페이지의 CRUD 와 개별 기능 사용내역을 운영자 계층에 맞는 범위로만 열람하도록 가시성을 분리했습니다. 독립사이트 관리자(L1 SaaS Admin)가 자기 SaaS 범위를 기본 열람하고, 보안/시스템 내부 이벤트는 Platform(L0) 전용으로 격리됩니다.
계층 컨텍스트 컬럼
기존 audit_logs 에 계층 가시성을 위한 두 컬럼이 추가되었습니다.
| 컬럼 | 설명 | 가시성 용도 |
|---|---|---|
saas_product_id | 기록 시점의 SaaS Product(독립사이트) | L1 SaaS Admin 스코프 |
organization_id | 기록 시점의 조직 | L3+ 조직 서브트리 스코프 |
- 마이그레이션:
2026_06_01_000002_add_saas_org_context_to_audit_logs.php(파티션 부모 ALTER → 자식 자동 상속, 멱등hasColumn가드, 인덱스 추가, FK 생략, 보존형 down) - 컨텍스트는
App\Core\Base\Audit\Support\AuditContext가 현재 요청에서 해석합니다.saasProductId():current.saas_product바인딩 → 세션(current_saas_product_id) → 인증 사용자saas_product_idorganizationId(): 인증 사용자organization_idtenantId(): 세션(tenant_id) → 사용자getTenantId()
- 기록 지점(
AuditObserver,AuditLog::logAction()/logSystem()/logView(),ActionLogger)이 이 컨텍스트를 자동으로 함께 저장합니다.
graceful 가드 — 미마이그레이션 프로젝트 보호
Core 는 symlink 로 여러 프로젝트가 공유합니다. 위 두 컬럼은 Phase B 마이그레이션으로 추가되는데, 아직 migrate 하지 않은 프로젝트의 운영 DB 에는 컬럼이 없습니다. 컬럼 부재 시 saas_product_id 를 INSERT 하면 column does not exist 500 이 발생하므로, 모든 기록 지점은 컬럼이 존재할 때만 계층 컨텍스트를 기록합니다.
App\Core\Base\Audit\Models\AuditLog::supportsHierarchicalContext() 가 Schema::hasColumn() 결과를 memoize 하여 이를 판정합니다. (true 로 확인되면 캐시, false/null 은 매 호출 재확인 — 테스트의 RefreshDatabase 나 마이그레이션 직후의 "없음→있음" 전이를 놓치지 않기 위함)
if (AuditLog::supportsHierarchicalContext()) {
$attributes['saas_product_id'] = AuditContext::saasProductId();
$attributes['organization_id'] = AuditContext::organizationId();
}
- 미마이그레이션 프로젝트: 계층 컨텍스트는 생략되고 나머지 로깅은 정상 동작(500 없음)
- 마이그레이션 후: 자동으로 계층 가시성 활성
- 기존 로그(saas/org 가 NULL 인 과거 기록)는 backfill 하지 않으며, 신규 로그부터 계층 스코프가 적용됩니다.
조회 표면 노출 게이트 (사이트 유형)
데이터 스코프(아래 buildQuery)가 적용되기 이전에, 감사 로그 화면(Resource) 자체의 노출 여부를 사이트 유형으로 게이트합니다 (BaseAuditLogResource::passesSiteTypeGate).
- 노출 = 독립사이트(미러 표면 포함) OR Platform(L0). 비독립 Tenant/Org 패널에서는 감사 로그 화면이 아예 노출되지 않습니다(네비게이션 메뉴 + 직접 URL 모두 차단 —
shouldRegisterNavigation/canAccess/canViewAny). - 게이트는 패널 정체성(소속 패널 Platform/SaaS/Tenant/Org — 런타임 현재 패널, 정적 컨텍스트는 namespace 추론)으로 판정하며, Resource 의 권한 레벨(
getMinimumLevel)에 의존하지 않습니다. 따라서 프로젝트가 감사 Resource 를 (쿼리 범위 제한 등 목적으로) 커스터마이즈해도 노출 게이트는 일관되게 동작합니다. - 미러 도메인 컨텍스트는 독립사이트로 resolve되어 자연히 통과합니다(미러는 독립사이트에 귀속).
설계: ADR-090(사이트 모델 — 2유형 + route mount) · ADR-087(관리자/감사 가시성). 노출 게이트를 통과한 패널에서만 아래 buildQuery 데이터 스코프가 적용됩니다.
패널별 AuditLogResource 스코프
각 패널의 감사 로그 화면은 App\Core\Base\Audit\Filament\Resources\BaseAuditLogResource 의 buildQuery() 로 운영자 레벨에 따라 자동 스코프됩니다.
| 레벨 | 패널 | 열람 범위 | 스코프 기준 |
|---|---|---|---|
| L0 | Platform | 전체 | 스코프 없음 |
| L1 | SaaS | 자기 SaaS | saas_product_id = {내 SaaS} |
| L2 | Tenant | 자기 테넌트 | tenant_id = {내 테넌트} |
| L3+ | Org | 자기 조직 서브트리(depth-무관) | organization_id ∈ ViewerScopeResolver::viewerOrgIds() |
- L3+ 에서 org 가 해석되지 않으면(
null) 보수적으로 차단(1 = 0)하여 과다 노출을 방지합니다. - 감사 로그 화면은 읽기 전용입니다(
canCreate()= false, 생성/수정/삭제 액션 없음).
이벤트 민감도 분리선 (visible_events)
패널별로 노출 가능한 이벤트 allowlist 를 config/core.php 의 audit.visible_events 로 분리합 니다. 계층 스코프(어떤 레코드)와 별개로, 민감 이벤트(어떤 종류)를 패널 단위로 통제합니다.
// config/core.php — audit.visible_events
'visible_events' => [
'platform' => ['created', 'updated', 'deleted', 'restored', 'force_deleted', 'viewed', 'login', 'logout', 'login_failed', 'password_reset', 'action'],
'saas' => ['created', 'updated', 'deleted', 'restored', 'login', 'logout', 'action'],
'tenant' => ['created', 'updated', 'deleted', 'action'],
'org' => ['created', 'updated', 'deleted', 'action'],
],
| 패널 | 노출 이벤트 | 비고 |
|---|---|---|
platform (L0) | CUD + restored/force_deleted + viewed + login/logout/login_failed/password_reset + action | 전체 — 보안/시스템 내부 이벤트 포함 |
saas (L1) | CUD + restored + login/logout + action | 보안 민감 이벤트 제외 |
tenant (L2) | CUD + action | 로그인/보안 이벤트 제외 |
org (L3) | CUD + action | 로그인/보안 이벤트 제외 — 조직 운영 범위 |
Platform 전용(보안/시스템 내부) 이벤트: login_failed, password_reset, viewed, force_deleted — 다른 패널 allowlist 에는 포함되지 않습니다.
'action' 이 포함된 패널은 CUD/인증 표준 이벤트가 아닌 커스텀 액션 이벤트(점 표기, 예: inquiry.escalate)도 일괄 노출합니다. BaseAuditLogResource 의 표준 이벤트 집합(created, updated, deleted, restored, force_deleted, login, logout, login_failed, password_reset, viewed, action) 이외의 모든 이벤트가 "action" 범주로 취급됩니다.
운영자가 보는 범위 요약
| 운영자 | 어떤 레코드 | 어떤 이벤트 |
|---|---|---|
| Platform Admin | 모든 SaaS/테넌트/조직 | 모든 이벤트(보안/시스템 포함) |
| SaaS Admin | 자기 SaaS Product | CUD + 로그인/로그아웃 + 커스텀 액션 |
| Tenant Admin | 자기 테넌트 | CUD + 커스텀 액션 |
| Org Admin (L3+) | 자기 조직 + 하위 조직 | CUD + 커스텀 액션 |
재사용 로깅 모듈 (ActionLogger / audited)
CRUD 는 Auditable Trait + AuditObserver 가 자동 기록하지만, 비-CRUD 액션(에스컬레이션, 내보내기, 배정 등)은 별도 기록이 필요합니다. 이를 위한 단일 진입점 로깅 모듈을 제공합니다 — "어떤 기능이든 모듈만 포함하면 자동 로깅".
세 가지 적용 방법
| 방법 | 적용 대상 | 사용 |
|---|---|---|
ActionLogger::log() | 어디서나 | app(ActionLogger::class)->log('export.users', $subject, $meta) |
LogsActions trait | 서비스 / Job / 핸들러 | use LogsActions; → $this->logAction($event, $subject, $meta) |
->audited('event') | Filament Action | Action 체인에 한 줄 추가 |
세 방법 모두 내부적으로 App\Core\Base\Audit\Services\ActionLogger 로 위임되며, 계층 컨텍스트(saas_product_id / organization_id / tenant_id)를 자동으로 함께 기록합니다. 로깅 실패가 기능 흐름을 막지 않도록 try/catch 로 감싸져 있습니다(실패 시 null 반환).
ActionLogger 서비스 / LogsActions trait
<?php
namespace App\Services;
use App\Core\Base\Audit\Traits\LogsActions;
use App\Models\Inquiry;
use App\Models\User;
class InquiryService
{
use LogsActions;
public function assign(Inquiry $inquiry, User $staff): void
{
// ... 배정 로직 ...
// 점 표기 액션 이름 권장 → visible_events 의 'action' 으로 노출
$this->logAction('inquiry.assign', $inquiry, ['staff_id' => $staff->id]);
}
}
ActionLogger::log() 의 시그니처:
| 인자 | 타입 | 설명 |
|---|---|---|
$event | string | 점 표기 액션 이름 (예: inquiry.assign, users.export) |
$subject | ?Model | 액션 대상 모델(선택) — auditable_type / auditable_id 로 기록 |
$metadata | array | 구조화 메타데이터(선택) |
Filament Action ->audited('event') 매크로
App\Core\Base\Audit 의 매크로는 CoreServiceProvider::boot() 에서 Filament\Actions\Action::macro('audited', ...) 로 등록됩니다. 어떤 Action 이든 체인에 한 줄만 추가하면, Action 실행 후(after() 훅) ActionLogger 로 자동 기록됩니다.
use Filament\Actions\Action;
Action::make('escalate')
->label('에스컬레이션')
->action(fn ($record) => $this->escalate($record))
->audited('inquiry.escalate');
메타데이터를 함께 기록하려면 두 번째 인자에 클로저를 전달합니다.
Action::make('escalate')
->audited('inquiry.escalate', fn ($record) => ['priority' => $record->priority]);
생성/수정/삭제(CRUD)는 AuditObserver 가 이미 자동 기록합니다(created / updated / deleted 이벤트). ->audited() 와 LogsActions 는 CRUD 외 비-CRUD 액션 기록에 사용하세요. CRUD 액션에 중복 적용하면 동일 작업이 두 번(updated + 커스텀 이벤트) 기록될 수 있습니다.
커스텀 액션의 가시성 노출
점 표기 커스텀 액션 이벤트(예: inquiry.escalate, users.export)는 표준 이벤트가 아니므로 패널 화면에서 visible_events 에 'action' 이 포함된 패널에서만 노출됩니다(앞 절의 민감도 분리선 참조). 즉 Platform / SaaS / Tenant / Org 모든 패널이 커스텀 액션을 열람할 수 있습니다.
실제 적용 예
| 기능 | 이벤트 | 적용 방식 |
|---|---|---|
| 문의 에스컬레이션 | inquiry.escalate | Filament Action ->audited('inquiry.escalate') |
| 회원 CSV 내보내기 | users.export | BaseAllUsersResource::exportUsersCsv 에서 스트림 직전 ActionLogger::log('users.export', ...) 명시 호출 |
->audited() 는 Action 의 after() 훅에 의존합니다. 파일 스트림 다운로드처럼 after() 훅 실행이 보장되지 않는 경우, 회원 CSV 내보내기처럼 스트림 직전에 ActionLogger::log() 를 직접 호출하는 것이 안전합니다.
관련 문서
- Core 개요 - 모듈 전체 구조
- Permission 모듈 - 권한 기반 접근 제어
- Tenant 모듈 - 테넌트 격리
- 보안 베스트 프랙티스 - 보안 가이드