'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); } }