Files
2026-05-11 11:29:41 +09:00

343 lines
9.4 KiB
PHP

<?php
namespace App\Models;
use App\Enums\ActivityLogType;
use App\Extension\ExtensionManager;
use App\Extension\HookManager;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* 활동 로그 모델
*
* 시스템 전체의 활동 이력을 관리하는 모델입니다.
* Polymorphic 관계를 통해 어떤 모델의 활동이든 기록할 수 있습니다.
* description_key + description_params 기반 실시간 다국어 번역을 지원합니다.
*/
class ActivityLog extends Model
{
/**
* 테이블명
*
* @var string
*/
protected $table = 'activity_logs';
/**
* 타임스탬프 설정
* created_at만 사용 (updated_at 없음)
*
* @var bool
*/
public $timestamps = false;
/**
* 대량 할당 가능한 속성
*
* @var array<int, string>
*/
protected $fillable = [
'log_type',
'loggable_type',
'loggable_id',
'user_id',
'action',
'description_key',
'description_params',
'properties',
'changes',
'ip_address',
'user_agent',
'created_at',
];
/**
* 속성 캐스팅
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'log_type' => ActivityLogType::class,
'description_params' => 'array',
'properties' => 'array',
'changes' => 'array',
'created_at' => 'datetime',
];
}
/**
* 모델 생성 시 created_at 자동 설정
*/
protected static function boot(): void
{
parent::boot();
static::creating(function (ActivityLog $model) {
if (! $model->created_at) {
$model->created_at = now();
}
});
}
/**
* 대상 모델 관계 (Polymorphic)
*
* @return MorphTo
*/
public function loggable(): MorphTo
{
return $this->morphTo();
}
/**
* 사용자 관계
*
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* 다국어 번역된 설명을 반환합니다.
*
* description_key와 description_params를 사용하여 실시간 번역합니다.
* 'core.activity_log.filter_description_params' 필터 훅을 통해
* 모듈이 properties의 ID를 엔티티 이름으로 변환할 수 있습니다.
*
* @return string
*/
public function getLocalizedDescriptionAttribute(): string
{
if ($this->description_key === null) {
return '';
}
$params = $this->description_params ?? [];
$params = HookManager::applyFilters(
'core.activity_log.filter_description_params',
$params,
$this->description_key,
$this->properties ?? []
);
// 다국어 배열 값을 현재 로케일 문자열로 변환 (__() 함수는 문자열만 치환 가능)
$params = array_map(function ($value) {
if (is_array($value)) {
$locale = app()->getLocale();
$fallback = config('app.fallback_locale', 'en');
return $value[$locale] ?? $value[$fallback] ?? (string) reset($value);
}
return $value;
}, $params);
return __($this->description_key, $params);
}
/**
* 로그 유형 라벨을 반환합니다.
*
* @return string
*/
public function getLogTypeLabelAttribute(): string
{
return $this->log_type->label();
}
/**
* 액션 라벨을 반환합니다.
*
* 5단계 조회: 모듈 lang 전체 키 → 모듈 lang 마지막 세그먼트
* → 코어 lang 전체 키 → 코어 lang 마지막 세그먼트 → raw 문자열 fallback
*
* 모듈/플러그인 origin 라벨은 자체 lang 파일에서 우선 조회되어 G7 의 영역 분리
* 설계 의도와 일치합니다. 모듈 lang 미정의 시 코어 lang fallback (하위 호환).
*
* Origin 식별 우선순위:
* 1) loggable_type FQCN (가장 정확)
* 2) properties.extension_origin (loggable 없는 케이스 — 호출 시점 trait 자동 주입)
*
* @return string
*/
public function getActionLabelAttribute(): string
{
$namespace = $this->resolveOriginNamespace();
$lastSegment = last(explode('.', $this->action));
if ($namespace !== null) {
// 1단계: 모듈 lang 의 전체 액션 키
$moduleFullKey = "{$namespace}::activity_log.action.{$this->action}";
$translated = __($moduleFullKey);
if ($translated !== $moduleFullKey) {
return $translated;
}
// 2단계: 모듈 lang 의 마지막 세그먼트
$moduleSegmentKey = "{$namespace}::activity_log.action.{$lastSegment}";
$translated = __($moduleSegmentKey);
if ($translated !== $moduleSegmentKey) {
return $translated;
}
}
// 3단계: 코어 lang 의 전체 액션 키
$fullKey = "activity_log.action.{$this->action}";
$translated = __($fullKey);
if ($translated !== $fullKey) {
return $translated;
}
// 4단계: 코어 lang 의 마지막 세그먼트
$segmentKey = "activity_log.action.{$lastSegment}";
$translated = __($segmentKey);
if ($translated !== $segmentKey) {
return $translated;
}
// 5단계: raw action 문자열 fallback
return $this->action;
}
/**
* 본 활동 로그의 origin 모듈/플러그인 lang 네임스페이스를 추론합니다.
*
* @return string|null 모듈/플러그인 식별자 (네임스페이스), 코어 origin 또는 미해석 시 null
*/
protected function resolveOriginNamespace(): ?string
{
if ($this->loggable_type !== null) {
$resolved = ExtensionManager::resolveExtensionByFqcn($this->loggable_type);
if ($resolved !== null) {
return $resolved;
}
}
$origin = $this->properties['extension_origin'] ?? null;
if (is_string($origin) && $origin !== '') {
return $origin;
}
return null;
}
/**
* 행위자 이름을 반환합니다.
*
* @return string
*/
public function getActorNameAttribute(): string
{
return $this->user?->name ?? __('common.system');
}
/**
* 대상 모델 타입의 짧은 표시명을 반환합니다.
*
* FQCN에서 클래스명만 추출합니다.
* 예: 'Modules\Sirsoft\Ecommerce\Models\Product' → 'Product'
*
* @return string|null
*/
public function getLoggableTypeDisplayAttribute(): ?string
{
if ($this->loggable_type === null) {
return null;
}
$parts = explode('\\', $this->loggable_type);
return end($parts);
}
/**
* 관리자 활동 로그만 필터링하는 스코프
*
* @param Builder $query 쿼리 빌더
* @return Builder 필터링된 쿼리 빌더
*/
public function scopeAdmin(Builder $query): Builder
{
return $query->where('log_type', ActivityLogType::Admin);
}
/**
* 사용자 활동 로그만 필터링하는 스코프
*
* @param Builder $query 쿼리 빌더
* @return Builder 필터링된 쿼리 빌더
*/
public function scopeUserLog(Builder $query): Builder
{
return $query->where('log_type', ActivityLogType::User);
}
/**
* 시스템 활동 로그만 필터링하는 스코프
*
* @param Builder $query 쿼리 빌더
* @return Builder 필터링된 쿼리 빌더
*/
public function scopeSystem(Builder $query): Builder
{
return $query->where('log_type', ActivityLogType::System);
}
/**
* 특정 모델에 대한 로그만 필터링하는 스코프
*
* @param Builder $query 쿼리 빌더
* @param Model $model 대상 모델
* @return Builder 필터링된 쿼리 빌더
*/
public function scopeForModel(Builder $query, Model $model): Builder
{
return $query
->where('loggable_type', $model->getMorphClass())
->where('loggable_id', $model->getKey());
}
/**
* 특정 액션에 대한 로그만 필터링하는 스코프
*
* @param Builder $query 쿼리 빌더
* @param string $action 액션 유형
* @return Builder 필터링된 쿼리 빌더
*/
public function scopeAction(Builder $query, string $action): Builder
{
return $query->where('action', $action);
}
/**
* 특정 사용자의 로그만 필터링하는 스코프
*
* @param Builder $query 쿼리 빌더
* @param int $userId 사용자 ID
* @return Builder 필터링된 쿼리 빌더
*/
public function scopeByUser(Builder $query, int $userId): Builder
{
return $query->where('user_id', $userId);
}
/**
* 최신순으로 정렬하는 스코프
*
* @param Builder $query 쿼리 빌더
* @return Builder 정렬된 쿼리 빌더
*/
public function scopeLatest(Builder $query): Builder
{
return $query->orderByDesc('created_at');
}
}