번들 확장 20개 전부에 AGENTS.md · README.md · docs/ 를 채우고, 코어가 들고 있던 확장 소유 문서 두 갈래를 그 확장으로 옮긴다. 번들 템플릿의 컴포넌트· 핸들러·레이아웃 상세와 확장이 구독하는 활동 로그 훅 목록이 그 대상이며, 코어에는 총계와 링크만 남아 확장이 기능을 늘릴 때 코어 문서를 고쳐야 하던 역방향 의존이 사라진다. 전수 완비를 확인하고 강제를 조인다 — 문서 동반 룰을 대상 목록 없는 error 로 승격하고, 검사 스크립트가 문서 미보유를 실패로 올리며, 미채움 마커 baseline 을 0 으로 기록한다. 한쪽만 조이면 "새 확장이 문서 없이 들어와도 초록" 인 상태가 남는데 그 결과는 이상 0건과 구분되지 않는다. 집필 과정에서 드러난 생성기 결함 셋을 함께 고친다. 스케줄 주기 열이 계약 키를 읽지 않아 모든 확장에서 '-' 였고, 네임스페이스를 붙인 핸들러 등록 키가 수집에서 통째로 빠졌으며, README 골격이 폐기된 히어로 배지를 계속 찍어내고 있었다. 셋 다 산출물이 아니라 원천이 틀린 것이라, 가드의 모집단에 생성기 출력 자체를 넣어 다음 확장이 같은 상태로 태어나는 경로를 막는다.
423 lines
19 KiB
Markdown
423 lines
19 KiB
Markdown
# 활동 로그 훅 레퍼런스 (Activity Log Hooks Reference)
|
|
|
|
> 모든 ActivityLog 리스너가 구독하는 훅의 전체 목록 (코어 + 모듈)
|
|
|
|
## TL;DR (5초 요약)
|
|
|
|
```text
|
|
1. 코어 66훅 + 확장 132훅 = 총 198훅 (확장별 목록은 그 확장이 소유)
|
|
2. Listener에서 Log::channel('activity')->info() 직접 호출 (Monolog → ActivityLogHandler → DB)
|
|
3. 스냅샷 패턴: before_update(priority 5) → 캡처, after_update → ChangeDetector로 비교
|
|
4. 사용자 행위: ActivityLogType::User (장바구니/위시리스트/쿠폰 다운로드/주문/결제)
|
|
5. 새 훅 추가 시: Listener에 구독 + lang 파일에 description_key 정의
|
|
```
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
1. [아키텍처 개요](#1-아키텍처-개요)
|
|
2. [코어 훅 (CoreActivityLogListener)](#2-코어-훅-coreactivityloglistener)
|
|
3. [확장 모듈 훅](#3-확장-모듈-훅)
|
|
4. [스냅샷/변경감지 패턴](#4-스냅샷변경감지-패턴)
|
|
5. [새 모듈에 ActivityLog 추가하기](#5-새-모듈에-activitylog-추가하기)
|
|
|
|
---
|
|
|
|
## 1. 아키텍처 개요
|
|
|
|
```text
|
|
Service → doAction('hook.name') → ActivityLogListener → Log::channel('activity') → ActivityLogHandler → DB
|
|
```
|
|
|
|
- **Listener**: `HookListenerInterface` 구현, `getSubscribedHooks()`로 훅-메서드 매핑 정의
|
|
- **로그 기록**: `Log::channel('activity')->info($action, $context)` 직접 호출
|
|
- **context 구조**: `log_type`, `loggable`, `description_key`, `description_params`, `properties`, `changes`
|
|
- **LogType**: `ActivityLogType::Admin` (관리자 행위) / `ActivityLogType::User` (사용자 행위)
|
|
- **변경 감지**: `ChangeDetector::detect($model, $snapshot)` — 스냅샷과 현재 상태 비교
|
|
|
|
---
|
|
|
|
## 2. 코어 훅 (CoreActivityLogListener)
|
|
|
|
**파일**: `app/Listeners/CoreActivityLogListener.php`
|
|
**총 66훅**
|
|
|
|
### User (8훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.user.after_create` | `handleUserAfterCreate` | `user.create` | Admin | User |
|
|
| `core.user.after_update` | `handleUserAfterUpdate` | `user.update` | Admin | User |
|
|
| `core.user.after_delete` | `handleUserAfterDelete` | `user.delete` | Admin | User |
|
|
| `core.user.after_withdraw` | `handleUserAfterWithdraw` | `user.withdraw` | Admin | User |
|
|
| `core.user.after_show` | `handleUserAfterShow` | `user.show` | Admin | User |
|
|
| `core.user.after_list` | `handleUserAfterList` | `user.list` | Admin | - |
|
|
| `core.user.after_search` | `handleUserAfterSearch` | `user.search` | Admin | - |
|
|
| `sirsoft-core.user.after_bulk_update` | `handleUserAfterBulkUpdate` | `user.bulk_update` | Admin | - |
|
|
|
|
### Auth (9훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.auth.after_login` | `handleAuthAfterLogin` | `auth.login` | User | User |
|
|
| `core.auth.logout` | `handleAuthLogout` | `auth.logout` | User | User |
|
|
| `core.auth.register` | `handleAuthRegister` | `auth.register` | User | User |
|
|
| `core.auth.forgot_password` | `handleAuthForgotPassword` | `auth.forgot_password` | User | - |
|
|
| `core.auth.reset_password` | `handleAuthResetPassword` | `auth.reset_password` | User | - |
|
|
| `core.auth.record_consents` | `handleAuthRecordConsents` | `auth.record_consents` | User | User |
|
|
| `core.auth.login_failed` | `handleAuthLoginFailed` | `auth.login_failed` | User | - |
|
|
| `core.auth.account_locked` | `handleAuthAccountLocked` | `auth.account_locked` | User | User |
|
|
| `core.auth.account_unlocked` | `handleAuthAccountUnlocked` | `auth.account_unlocked` | User | User |
|
|
|
|
### Identity (3훅)
|
|
|
|
본인인증(IDV) 훅은 DTO(`VerificationChallenge`/`VerificationResult`)를 인자로 받으므로 `sync => true` 로 등록된다 — 큐 직렬화 대상에 POPO 가 포함되지 않아 큐로 넘기면 `null` 이 전달된다.
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.identity.after_request` | `handleIdentityRequested` | `identity.request` | User | - |
|
|
| `core.identity.after_verify` | `handleIdentityVerified` | `identity.verify` / `identity.verify_failed` | User | - |
|
|
| `core.identity.challenge_expired` | `handleIdentityExpired` | `identity.expired` | User | - |
|
|
|
|
### Role (5훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.role.after_create` | `handleRoleAfterCreate` | `role.create` | Admin | Role |
|
|
| `core.role.after_update` | `handleRoleAfterUpdate` | `role.update` | Admin | Role |
|
|
| `core.role.after_delete` | `handleRoleAfterDelete` | `role.delete` | Admin | Role |
|
|
| `core.role.after_sync_permissions` | `handleRoleAfterSyncPermissions` | `role.sync_permissions` | Admin | Role |
|
|
| `core.role.after_toggle_status` | `handleRoleAfterToggleStatus` | `role.toggle_status` | Admin | Role |
|
|
|
|
### Menu (6훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.menu.after_create` | `handleMenuAfterCreate` | `menu.create` | Admin | Menu |
|
|
| `core.menu.after_update` | `handleMenuAfterUpdate` | `menu.update` | Admin | Menu |
|
|
| `core.menu.after_delete` | `handleMenuAfterDelete` | `menu.delete` | Admin | Menu |
|
|
| `core.menu.after_update_order` | `handleMenuAfterUpdateOrder` | `menu.update_order` | Admin | - |
|
|
| `core.menu.after_toggle_status` | `handleMenuAfterToggleStatus` | `menu.toggle_status` | Admin | Menu |
|
|
| `core.menu.after_sync_roles` | `handleMenuAfterSyncRoles` | `menu.sync_roles` | Admin | Menu |
|
|
|
|
### Settings (2훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.settings.after_save` | `handleSettingsAfterSave` | `settings.save` | Admin | - |
|
|
| `core.settings.after_set` | `handleSettingsAfterSet` | `settings.set` | Admin | - |
|
|
|
|
### Schedule (6훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.schedule.after_create` | `handleScheduleAfterCreate` | `schedule.create` | Admin | Schedule |
|
|
| `core.schedule.after_update` | `handleScheduleAfterUpdate` | `schedule.update` | Admin | Schedule |
|
|
| `core.schedule.after_delete` | `handleScheduleAfterDelete` | `schedule.delete` | Admin | Schedule |
|
|
| `core.schedule.after_run` | `handleScheduleAfterRun` | `schedule.run` | Admin | Schedule |
|
|
| `core.schedule.after_bulk_update` | `handleScheduleAfterBulkUpdate` | `schedule.bulk_update` | Admin | - |
|
|
| `core.schedule.after_bulk_delete` | `handleScheduleAfterBulkDelete` | `schedule.bulk_delete` | Admin | - |
|
|
|
|
### Attachment (3훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.attachment.after_upload` | `handleAttachmentAfterUpload` | `attachment.upload` | Admin | Attachment |
|
|
| `core.attachment.after_delete` | `handleAttachmentAfterDelete` | `attachment.delete` | Admin | Attachment |
|
|
| `core.attachment.after_bulk_delete` | `handleAttachmentAfterBulkDelete` | `attachment.bulk_delete` | Admin | - |
|
|
|
|
### Module (6훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.modules.after_install` | `handleModuleAfterInstall` | `module.install` | Admin | - |
|
|
| `core.modules.after_activate` | `handleModuleAfterActivate` | `module.activate` | Admin | - |
|
|
| `core.modules.after_deactivate` | `handleModuleAfterDeactivate` | `module.deactivate` | Admin | - |
|
|
| `core.modules.after_uninstall` | `handleModuleAfterUninstall` | `module.uninstall` | Admin | - |
|
|
| `core.modules.after_update` | `handleModuleAfterUpdate` | `module.update` | Admin | - |
|
|
| `core.modules.after_refresh_layouts` | `handleModuleAfterRefreshLayouts` | `module.refresh_layouts` | Admin | - |
|
|
|
|
### Plugin (5훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.plugins.after_install` | `handlePluginAfterInstall` | `plugin.install` | Admin | - |
|
|
| `core.plugins.after_activate` | `handlePluginAfterActivate` | `plugin.activate` | Admin | - |
|
|
| `core.plugins.after_deactivate` | `handlePluginAfterDeactivate` | `plugin.deactivate` | Admin | - |
|
|
| `core.plugins.after_uninstall` | `handlePluginAfterUninstall` | `plugin.uninstall` | Admin | - |
|
|
| `core.plugins.after_update` | `handlePluginAfterUpdate` | `plugin.update` | Admin | - |
|
|
|
|
### Template (6훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.templates.after_install` | `handleTemplateAfterInstall` | `template.install` | Admin | - |
|
|
| `core.templates.after_activate` | `handleTemplateAfterActivate` | `template.activate` | Admin | - |
|
|
| `core.templates.after_deactivate` | `handleTemplateAfterDeactivate` | `template.deactivate` | Admin | - |
|
|
| `core.templates.after_uninstall` | `handleTemplateAfterUninstall` | `template.uninstall` | Admin | - |
|
|
| `core.templates.after_version_update` | `handleTemplateAfterVersionUpdate` | `template.version_update` | Admin | - |
|
|
| `core.templates.after_refresh_layouts` | `handleTemplateAfterRefreshLayouts` | `template.refresh_layouts` | Admin | - |
|
|
|
|
### Extension 공통 (1훅)
|
|
|
|
템플릿·모듈·플러그인 세 타입이 같은 훅을 공유한다 (권한도 `core.extensions.custom_assets.manage` 하나다).
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.custom_assets.after_change` | `handleCustomAssetAfterChange` | `custom_asset.{save\|upload\|delete}` | Admin | 운영자 CSS·JS 는 사이트 전 화면에서 실행되므로 변경 이력이 남아야 한다 |
|
|
|
|
### Layout (2훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.layout.after_update` | `handleLayoutAfterUpdate` | `layout.update` | Admin | - |
|
|
| `core.layout.after_version_restore` | `handleLayoutAfterVersionRestore` | `layout.version_restore` | Admin | - |
|
|
|
|
### Module Settings (2훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.module_settings.after_save` | `handleModuleSettingsAfterSave` | `module_settings.save` | Admin | - |
|
|
| `core.module_settings.after_reset` | `handleModuleSettingsAfterReset` | `module_settings.reset` | Admin | - |
|
|
|
|
### Plugin Settings (2훅)
|
|
|
|
| 훅 이름 | Listener 메서드 | Action (DB) | LogType | Loggable |
|
|
|---------|----------------|-------------|---------|----------|
|
|
| `core.plugin_settings.after_save` | `handlePluginSettingsAfterSave` | `plugin_settings.save` | Admin | - |
|
|
| `core.plugin_settings.after_reset` | `handlePluginSettingsAfterReset` | `plugin_settings.reset` | Admin | - |
|
|
|
|
---
|
|
|
|
## 3. 확장 모듈 훅
|
|
|
|
확장이 구독하는 활동 로그 훅 목록은 **그 확장이 소유**합니다(#601). 확장이 훅을 추가할 때
|
|
코어 문서를 고쳐야 하는 역방향 의존을 없애기 위해서이며, 코어에는 아래 총계와 링크만 남습니다.
|
|
|
|
| 확장 | 훅 수 | 문서 |
|
|
|------|------|------|
|
|
| `sirsoft-ecommerce` | 92 | [docs/extension-points.md](../../modules/_bundled/sirsoft-ecommerce/docs/extension-points.md) |
|
|
| `sirsoft-board` | 30 | [docs/extension-points.md](../../modules/_bundled/sirsoft-board/docs/extension-points.md) |
|
|
| `sirsoft-page` | 7 | [docs/extension-points.md](../../modules/_bundled/sirsoft-page/docs/extension-points.md) |
|
|
| `sirsoft-pay_kginicis` | 1 | [docs/extension-points.md](../../plugins/_bundled/sirsoft-pay_kginicis/docs/extension-points.md) |
|
|
| `sirsoft-pay_nhnkcp` | 1 | [docs/extension-points.md](../../plugins/_bundled/sirsoft-pay_nhnkcp/docs/extension-points.md) |
|
|
| `sirsoft-pay_nicepayments` | 1 | [docs/extension-points.md](../../plugins/_bundled/sirsoft-pay_nicepayments/docs/extension-points.md) |
|
|
| **합계** | **132** | |
|
|
|
|
코어 66훅 + 확장 132훅 = **총 198훅**입니다.
|
|
|
|
확장에 새 활동 로그 항목을 추가할 때도 **다국어 키는 코어가 SSoT** 입니다 — action 라벨과
|
|
description 본문을 코어 `lang/{ko,en}/activity_log.php` 에 정의해야 하며, 모듈 lang 파일에
|
|
넣으면 해석되지 않습니다. 번들 일본어 팩도 같은 작업 단위에서 동기화합니다.
|
|
|
|
---
|
|
|
|
## 4. 스냅샷/변경감지 패턴
|
|
|
|
ActivityLog에서 수정(update) 작업의 변경 이력을 기록하려면 **스냅샷 패턴**을 사용합니다.
|
|
|
|
### 동작 흐름
|
|
|
|
```text
|
|
1. Service에서 before_update 훅 발행 (doAction)
|
|
2. Listener의 캡처 메서드 실행 (priority: 5, 다른 리스너보다 먼저)
|
|
→ 현재 모델 상태를 $snapshots 배열에 저장
|
|
3. Service에서 실제 DB 업데이트 수행
|
|
4. Service에서 after_update 훅 발행 (doAction)
|
|
5. Listener의 핸들러 메서드 실행 (priority: 20)
|
|
→ ChangeDetector::detect($model, $snapshot) 호출
|
|
→ 변경된 필드만 추출하여 로그 기록
|
|
→ 스냅샷 정리 (unset)
|
|
```
|
|
|
|
### 코드 예시
|
|
|
|
```php
|
|
// 1. getSubscribedHooks()에서 before/after 쌍 등록
|
|
public static function getSubscribedHooks(): array
|
|
{
|
|
return [
|
|
'my-module.entity.before_update' => ['method' => 'captureSnapshot', 'priority' => 5],
|
|
'my-module.entity.after_update' => ['method' => 'handleAfterUpdate', 'priority' => 20],
|
|
];
|
|
}
|
|
|
|
// 2. 스냅샷 캡처 (before_update, priority 5)
|
|
public function captureSnapshot(Model $entity, array $data): void
|
|
{
|
|
$this->snapshots['entity_' . $entity->id] = $entity->toArray();
|
|
}
|
|
|
|
// 3. 변경 감지 + 로그 기록 (after_update, priority 20)
|
|
public function handleAfterUpdate(Model $entity): void
|
|
{
|
|
$snapshot = $this->snapshots['entity_' . $entity->id] ?? null;
|
|
$changes = ChangeDetector::detect($entity, $snapshot);
|
|
|
|
Log::channel('activity')->info('entity.update', [
|
|
'log_type' => ActivityLogType::Admin,
|
|
'loggable' => $entity,
|
|
'description_key' => 'my-module::activity_log.description.entity_update',
|
|
'description_params' => ['entity_id' => $entity->id],
|
|
'changes' => $changes,
|
|
]);
|
|
|
|
unset($this->snapshots['entity_' . $entity->id]);
|
|
}
|
|
```
|
|
|
|
### ChangeDetector
|
|
|
|
**파일**: `app/ActivityLog/ChangeDetector.php`
|
|
|
|
- 모델의 `$activityLogFields` 속성에 정의된 필드만 비교
|
|
- 각 필드의 `label_key` (다국어 키)를 포함한 구조화된 변경 이력 생성
|
|
- `BackedEnum` 자동 변환 지원
|
|
- 스냅샷이 `null`이면 `null` 반환 (변경 없음)
|
|
|
|
### 스냅샷은 Service 가 잡아 넘긴다
|
|
|
|
리스너는 `before_*` 훅을 구독해 스냅샷을 잡지 않습니다. **Service 가 수정 직전에 스냅샷을
|
|
만들어 `after_*` 훅의 인자로 넘기고**, 리스너는 그것을 `ChangeDetector::detect()` 에 그대로
|
|
전달합니다:
|
|
|
|
```php
|
|
// Service (예: ProductService::update())
|
|
HookManager::doAction('sirsoft-ecommerce.product.after_update', $product, $snapshot);
|
|
|
|
// Listener
|
|
public function handleProductAfterUpdate(Product $product, ?array $snapshot = null): void
|
|
{
|
|
$changes = ChangeDetector::detect($product, $snapshot);
|
|
// ...
|
|
}
|
|
```
|
|
|
|
`before_*` 훅 자체는 발행되므로 다른 확장이 구독할 수 있습니다 — 다만 **활동 로그 리스너의
|
|
구독 대상은 아닙니다.** 확장별 구독 목록에서 `before_*` 가 보이지 않는 것은 누락이 아니라
|
|
이 구조 때문입니다.
|
|
|
|
---
|
|
|
|
## 5. 새 모듈에 ActivityLog 추가하기
|
|
|
|
### Step 1: Listener 클래스 생성
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace Modules\Vendor\MyModule\Listeners;
|
|
|
|
use App\ActivityLog\ChangeDetector;
|
|
use App\Contracts\Extension\HookListenerInterface;
|
|
use App\Enums\ActivityLogType;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class MyModuleActivityLogListener implements HookListenerInterface
|
|
{
|
|
private array $snapshots = [];
|
|
|
|
public static function getSubscribedHooks(): array
|
|
{
|
|
return [
|
|
'vendor-mymodule.entity.after_create' => ['method' => 'handleAfterCreate', 'priority' => 20],
|
|
'vendor-mymodule.entity.before_update' => ['method' => 'captureSnapshot', 'priority' => 5],
|
|
'vendor-mymodule.entity.after_update' => ['method' => 'handleAfterUpdate', 'priority' => 20],
|
|
'vendor-mymodule.entity.after_delete' => ['method' => 'handleAfterDelete', 'priority' => 20],
|
|
];
|
|
}
|
|
|
|
public function handle(...$args): void
|
|
{
|
|
// 개별 메서드에서 처리
|
|
}
|
|
|
|
// ... 각 핸들러 메서드 구현
|
|
}
|
|
```
|
|
|
|
### Step 2: module.php에 Listener 등록
|
|
|
|
```php
|
|
// module.php의 listeners 배열에 추가
|
|
'listeners' => [
|
|
MyModuleActivityLogListener::class,
|
|
],
|
|
```
|
|
|
|
### Step 3: 다국어 파일에 description_key 정의
|
|
|
|
```php
|
|
// lang/ko/activity_log.php
|
|
return [
|
|
'description' => [
|
|
'entity_create' => ':entity_name 엔티티가 생성되었습니다.',
|
|
'entity_update' => ':entity_name 엔티티가 수정되었습니다.',
|
|
'entity_delete' => ':entity_name 엔티티가 삭제되었습니다.',
|
|
],
|
|
];
|
|
```
|
|
|
|
### Step 4: 모델에 $activityLogFields 정의 (ChangeDetector 사용 시)
|
|
|
|
```php
|
|
// Model 클래스에 추가
|
|
protected array $activityLogFields = [
|
|
'name' => ['label_key' => 'vendor-mymodule::activity_log.fields.name'],
|
|
'status' => ['label_key' => 'vendor-mymodule::activity_log.fields.status'],
|
|
];
|
|
```
|
|
|
|
### Step 5: 테스트 작성
|
|
|
|
```php
|
|
// tests/Unit/Listeners/MyModuleActivityLogListenerTest.php
|
|
class MyModuleActivityLogListenerTest extends TestCase
|
|
{
|
|
public function test_getSubscribedHooks_returns_expected_hooks(): void
|
|
{
|
|
$hooks = MyModuleActivityLogListener::getSubscribedHooks();
|
|
|
|
$this->assertArrayHasKey('vendor-mymodule.entity.after_create', $hooks);
|
|
$this->assertArrayHasKey('vendor-mymodule.entity.before_update', $hooks);
|
|
// ...
|
|
}
|
|
|
|
public function test_handleAfterCreate_logs_activity(): void
|
|
{
|
|
Log::shouldReceive('channel')
|
|
->with('activity')
|
|
->andReturnSelf();
|
|
Log::shouldReceive('info')
|
|
->once()
|
|
->with('entity.create', Mockery::type('array'));
|
|
|
|
$listener = new MyModuleActivityLogListener();
|
|
$listener->handleAfterCreate($entity, $data);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 훅 네이밍 규칙
|
|
|
|
| 패턴 | 용도 | Priority |
|
|
|------|------|----------|
|
|
| `{module}.{entity}.before_update` | 스냅샷 캡처 | 5 (높은 우선순위) |
|
|
| `{module}.{entity}.after_create` | 생성 로그 | 20 |
|
|
| `{module}.{entity}.after_update` | 수정 로그 (변경감지) | 20 |
|
|
| `{module}.{entity}.after_delete` | 삭제 로그 | 20 |
|
|
| `{module}.{entity}.after_bulk_*` | 일괄 작업 로그 | 20 |
|
|
| `{module}.{entity}.after_toggle_*` | 상태 전환 로그 | 20 |
|
|
|
|
### context 구조
|
|
|
|
| 키 | 타입 | 설명 | 필수 |
|
|
|----|------|------|------|
|
|
| `log_type` | `ActivityLogType` | Admin 또는 User | O |
|
|
| `loggable` | `Model` | 대상 모델 (polymorphic) | - |
|
|
| `description_key` | `string` | 다국어 설명 키 | O |
|
|
| `description_params` | `array` | 설명 파라미터 | - |
|
|
| `properties` | `array` | 추가 속성 (JSON) | - |
|
|
| `changes` | `array\|null` | ChangeDetector 결과 | - |
|