- Auditable 트레이트 신규 생성 (bootAuditable 패턴) - creating: created_by/updated_by 자동 채우기 - updating: updated_by 자동 채우기 - deleting: deleted_by 채우기 + saveQuietly() - created/updated/deleted: audit_logs 자동 기록 - 기존 AuditLogger 패턴과 동일한 try/catch 조용한 실패 - 변경된 필드만 before/after 기록 (updated 이벤트) - auditExclude 프로퍼티로 모델별 제외 필드 설정 가능 - 제외 대상: Attendance, StockTransaction, TodayIssue 등 고빈도/시스템 모델 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
277 lines
6.5 KiB
PHP
277 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Commons;
|
|
|
|
use App\Models\Scopes\TenantScope;
|
|
use App\Traits\Auditable;
|
|
use App\Traits\BelongsToTenant;
|
|
use App\Traits\ModelTrait;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
/**
|
|
* @mixin IdeHelperMenu
|
|
*/
|
|
class Menu extends Model
|
|
{
|
|
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
|
|
|
protected $fillable = [
|
|
'tenant_id', 'parent_id', 'global_menu_id', 'name', 'url', 'is_active', 'sort_order',
|
|
'hidden', 'is_customized', 'is_external', 'external_url', 'icon', 'options',
|
|
'created_by', 'updated_by', 'deleted_by',
|
|
];
|
|
|
|
protected $hidden = [
|
|
'created_by',
|
|
'updated_by',
|
|
'deleted_by',
|
|
'deleted_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'is_active' => 'boolean',
|
|
'hidden' => 'boolean',
|
|
'is_customized' => 'boolean',
|
|
'is_external' => 'boolean',
|
|
'options' => 'array',
|
|
];
|
|
|
|
/**
|
|
* 상위 메뉴
|
|
*/
|
|
public function parent(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Menu::class, 'parent_id');
|
|
}
|
|
|
|
/**
|
|
* 하위 메뉴 목록
|
|
*/
|
|
public function children(): HasMany
|
|
{
|
|
return $this->hasMany(Menu::class, 'parent_id');
|
|
}
|
|
|
|
/**
|
|
* 원본 글로벌 메뉴 (테넌트 메뉴인 경우)
|
|
*/
|
|
public function globalMenu(): BelongsTo
|
|
{
|
|
return $this->belongsTo(GlobalMenu::class, 'global_menu_id');
|
|
}
|
|
|
|
/**
|
|
* 글로벌 메뉴에서 복제된 메뉴인지 확인
|
|
*/
|
|
public function isClonedFromGlobal(): bool
|
|
{
|
|
return ! is_null($this->global_menu_id);
|
|
}
|
|
|
|
/**
|
|
* 테넌트가 커스터마이징한 메뉴인지 확인
|
|
*/
|
|
public function isCustomized(): bool
|
|
{
|
|
return $this->is_customized;
|
|
}
|
|
|
|
/**
|
|
* 공유(NULL) + 현재 테넌트 모두 포함해서 조회
|
|
* (SoftDeletes 글로벌 스코프는 그대로 유지)
|
|
*/
|
|
public function scopeWithShared($query, ?int $tenantId = null)
|
|
{
|
|
$tenantId = $tenantId ?? app('tenant_id');
|
|
|
|
return $query
|
|
->withoutGlobalScope(TenantScope::class)
|
|
->where(function ($w) use ($tenantId) {
|
|
if (is_null($tenantId)) {
|
|
$w->whereNull('tenant_id');
|
|
} else {
|
|
$w->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 글로벌 메뉴만 조회
|
|
*/
|
|
public function scopeGlobal($query)
|
|
{
|
|
return $query
|
|
->withoutGlobalScope(TenantScope::class)
|
|
->whereNull('tenant_id');
|
|
}
|
|
|
|
/**
|
|
* 활성화된 메뉴만 조회
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
/**
|
|
* 숨겨지지 않은 메뉴만 조회
|
|
*/
|
|
public function scopeVisible($query)
|
|
{
|
|
return $query->where('hidden', false);
|
|
}
|
|
|
|
/**
|
|
* 최상위 메뉴만 조회
|
|
*/
|
|
public function scopeRoots($query)
|
|
{
|
|
return $query->whereNull('parent_id');
|
|
}
|
|
|
|
/**
|
|
* 동기화 비교를 위한 필드 목록
|
|
*/
|
|
public static function getSyncFields(): array
|
|
{
|
|
return ['name', 'url', 'icon', 'sort_order', 'is_active', 'hidden', 'is_external', 'external_url', 'options'];
|
|
}
|
|
|
|
// ============================================================
|
|
// Options JSON 헬퍼 메서드
|
|
// ============================================================
|
|
|
|
/**
|
|
* options에서 특정 키 값 조회
|
|
*/
|
|
public function getOption(string $key, mixed $default = null): mixed
|
|
{
|
|
return data_get($this->options, $key, $default);
|
|
}
|
|
|
|
/**
|
|
* options에 특정 키 값 설정
|
|
*/
|
|
public function setOption(string $key, mixed $value): static
|
|
{
|
|
$options = $this->options ?? [];
|
|
data_set($options, $key, $value);
|
|
$this->options = $options;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 라우트명 조회 (mng, api, react 등에서 사용)
|
|
*/
|
|
public function getRouteName(): ?string
|
|
{
|
|
return $this->getOption('route_name');
|
|
}
|
|
|
|
/**
|
|
* 메뉴 섹션 조회 (main, tools, labs 등)
|
|
*/
|
|
public function getSection(): string
|
|
{
|
|
return $this->getOption('section', 'main');
|
|
}
|
|
|
|
/**
|
|
* 메뉴 타입 조회 (normal, tool, lab 등)
|
|
*/
|
|
public function getMenuType(): string
|
|
{
|
|
return $this->getOption('menu_type', 'normal');
|
|
}
|
|
|
|
/**
|
|
* 필요 역할 조회
|
|
*/
|
|
public function getRequiresRole(): ?string
|
|
{
|
|
return $this->getOption('requires_role');
|
|
}
|
|
|
|
/**
|
|
* 특정 역할이 필요한지 확인
|
|
*/
|
|
public function requiresRole(?string $role = null): bool
|
|
{
|
|
$requiredRole = $this->getRequiresRole();
|
|
|
|
if ($requiredRole === null) {
|
|
return false;
|
|
}
|
|
|
|
if ($role === null) {
|
|
return true; // 역할이 필요함
|
|
}
|
|
|
|
return $requiredRole === $role;
|
|
}
|
|
|
|
/**
|
|
* Blade 컴포넌트명 조회
|
|
*/
|
|
public function getBladeComponent(): ?string
|
|
{
|
|
return $this->getOption('blade_component');
|
|
}
|
|
|
|
/**
|
|
* CSS 클래스 조회
|
|
*/
|
|
public function getCssClass(): ?string
|
|
{
|
|
return $this->getOption('css_class');
|
|
}
|
|
|
|
/**
|
|
* meta 데이터 조회 (앱별 커스텀 데이터)
|
|
*/
|
|
public function getMeta(?string $key = null, mixed $default = null): mixed
|
|
{
|
|
if ($key === null) {
|
|
return $this->getOption('meta', []);
|
|
}
|
|
|
|
return $this->getOption("meta.{$key}", $default);
|
|
}
|
|
|
|
/**
|
|
* meta 데이터 설정
|
|
*/
|
|
public function setMeta(string $key, mixed $value): static
|
|
{
|
|
return $this->setOption("meta.{$key}", $value);
|
|
}
|
|
|
|
/**
|
|
* 특정 섹션 메뉴만 조회
|
|
*/
|
|
public function scopeSection($query, string $section)
|
|
{
|
|
return $query->whereJsonContains('options->section', $section);
|
|
}
|
|
|
|
/**
|
|
* 특정 메뉴 타입만 조회
|
|
*/
|
|
public function scopeMenuType($query, string $type)
|
|
{
|
|
return $query->whereJsonContains('options->menu_type', $type);
|
|
}
|
|
|
|
/**
|
|
* 특정 역할이 필요한 메뉴만 조회
|
|
*/
|
|
public function scopeRequiringRole($query, string $role)
|
|
{
|
|
return $query->whereJsonContains('options->requires_role', $role);
|
|
}
|
|
}
|