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

261 lines
7.2 KiB
PHP

<?php
namespace App\Models;
use App\Enums\ExtensionOwnerType;
use App\Models\Concerns\HasUserOverrides;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* 역할 모델.
*
* 관리자 UI 에서 수정한 필드(name/description) 는 `HasUserOverrides` trait 에 의해
* `user_overrides` 컬럼에 자동 누적 기록되며, 이후 코어/확장 업데이트의 `syncRole()`
* 경로에서 필드 단위로 보존됩니다.
*/
class Role extends Model
{
use HasFactory, HasUserOverrides;
/**
* `HasUserOverrides` 가 추적할 필드.
*
* `ExtensionRoleSyncHelper::syncRole()` L79~85 의 upsert 시 필드 단위 보존 대상과 일치.
*
* @var array<int, string>
*/
protected array $trackableFields = ['name', 'description'];
/**
* 다국어 JSON 컬럼 — sub-key dot-path 단위로 user_overrides 보존.
*
* 사용자가 ko 라벨만 수정하면 user_overrides=['name.ko'] 로 기록되어
* 언어팩 활성/시더 재실행 시 ja/en 키는 자동 동기화 가능.
*
* @var array<int, string>
*/
protected array $translatableTrackableFields = ['name', 'description'];
/** @var array<string, array> 활동 로그 추적 필드 */
public static array $activityLogFields = [
'identifier' => ['label_key' => 'activity_log.fields.identifier', 'type' => 'text'],
'is_active' => ['label_key' => 'activity_log.fields.is_active', 'type' => 'boolean'],
];
/**
* 테이블명
*
* @var string
*/
protected $table = 'roles';
/**
* 기본키
*
* @var string
*/
protected $primaryKey = 'id';
/**
* 타임스탬프 사용 여부
*
* @var bool
*/
public $timestamps = true;
protected $fillable = [
'identifier',
'name',
'description',
'extension_type',
'extension_identifier',
'user_overrides',
'is_active',
];
/**
* 속성 캐스팅
*/
protected function casts(): array
{
return [
'name' => 'array',
'description' => 'array',
'extension_type' => ExtensionOwnerType::class,
'user_overrides' => 'array',
'is_active' => 'boolean',
];
}
/**
* 역할에 속한 사용자들과의 관계를 정의합니다.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_roles')
->withPivot(['assigned_at', 'assigned_by'])
->withTimestamps();
}
/**
* 역할이 가진 권한들과의 관계를 정의합니다.
*/
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class, 'role_permissions')
->withPivot(['granted_at', 'granted_by', 'scope_type'])
->withTimestamps();
}
/**
* 역할이 접근 가능한 메뉴들과의 관계
*
* @return BelongsToMany
*/
public function menus(): BelongsToMany
{
return $this->belongsToMany(Menu::class, 'role_menus')
->withPivot('permission_type')
->withTimestamps();
}
/**
* 역할의 메뉴 권한들과의 관계를 정의합니다.
*
* @deprecated role_menus 피벗 테이블 사용. menus() 메서드를 사용하세요.
* @return HasMany
*/
public function menuPermissions(): HasMany
{
return $this->hasMany(MenuPermission::class);
}
/**
* 특정 권한을 가지고 있는지 확인합니다.
*
* @param string $permission 권한 식별자
*/
public function hasPermission(string $permission): bool
{
return $this->permissions()->where('identifier', $permission)->exists();
}
/**
* 여러 권한을 가지고 있는지 확인합니다.
*
* @param array $permissions 권한 식별자 배열
* @param bool $requireAll 모든 권한이 필요한지 여부 (true: AND, false: OR)
*/
public function hasPermissions(array $permissions, bool $requireAll = true): bool
{
$userPermissions = $this->permissions()->whereIn('identifier', $permissions)->count();
return $requireAll ? $userPermissions === count($permissions) : $userPermissions > 0;
}
/**
* 특정 메뉴에 대한 권한을 가지고 있는지 확인합니다.
*
* @param int $menuId 메뉴 ID
* @param string $permissionType 권한 유형 (read, write, delete)
*/
public function hasMenuPermission(int $menuId, string $permissionType = 'read'): bool
{
return $this->menuPermissions()
->where('menu_id', $menuId)
->where('permission_type', $permissionType)
->where('is_allowed', true)
->exists();
}
/**
* 지정된 로케일의 역할 이름 반환
*/
public function getLocalizedName(?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
if (! is_array($this->name)) {
return (string) $this->name;
}
return $this->name[$locale]
?? $this->name[config('app.fallback_locale')]
?? (! empty($this->name) ? array_values($this->name)[0] : '')
?? '';
}
/**
* 지정된 로케일의 역할 설명 반환
*/
public function getLocalizedDescription(?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
if (! is_array($this->description)) {
return (string) $this->description;
}
return $this->description[$locale]
?? $this->description[config('app.fallback_locale')]
?? (! empty($this->description) ? array_values($this->description)[0] : '')
?? '';
}
/**
* 현재 로케일의 역할 이름 반환
*/
protected function localizedName(): Attribute
{
return Attribute::make(
get: fn () => $this->getLocalizedName()
);
}
/**
* 현재 로케일의 역할 설명 반환
*/
protected function localizedDescription(): Attribute
{
return Attribute::make(
get: fn () => $this->getLocalizedDescription()
);
}
/**
* 코어 시스템에서 생성된 역할인지 확인합니다.
*
* @return bool
*/
public function isCore(): bool
{
return $this->extension_type === ExtensionOwnerType::Core;
}
/**
* 삭제 가능한 역할인지 확인합니다.
*
* extension_type이 null(사용자 직접 생성)인 경우에만 삭제 가능합니다.
*
* @return bool
*/
public function isDeletable(): bool
{
return $this->extension_type === null;
}
/**
* 확장(모듈/플러그인)이 소유한 역할인지 확인합니다.
*
* @return bool
*/
public function isExtensionOwned(): bool
{
return in_array($this->extension_type, [ExtensionOwnerType::Module, ExtensionOwnerType::Plugin], true);
}
}