feat: Menu 모델 options 지원 및 MngMenuSeeder 추가

- Menu 모델에 options JSON 필드 및 헬퍼 메서드 추가
  - getOption(), setOption()
  - getRouteName(), getSection(), getMenuType()
  - requiresRole(), getBladeComponent(), getCssClass()
  - getMeta(), setMeta()
- MngMenuSeeder 추가 (tenant_id=1 기본 메뉴 29개)
  - main 섹션: 대시보드, PM, 시스템, 권한, 콘텐츠, 시스템설정
  - tools 섹션: 개발 도구
  - labs 섹션: 실험실 (S/A/M 탭)
This commit is contained in:
2025-12-16 14:47:55 +09:00
parent 5f453054b1
commit 22f07069e0
2 changed files with 633 additions and 2 deletions

View File

@@ -17,7 +17,7 @@ class Menu extends Model
protected $fillable = [
'tenant_id', 'parent_id', 'global_menu_id', 'name', 'url', 'is_active', 'sort_order',
'hidden', 'is_external', 'external_url', 'icon', 'is_customized',
'hidden', 'is_external', 'external_url', 'icon', 'is_customized', 'options',
'created_by', 'updated_by', 'deleted_by',
];
@@ -33,6 +33,7 @@ class Menu extends Model
'hidden' => 'boolean',
'is_external' => 'boolean',
'is_customized' => 'boolean',
'options' => 'array',
];
/**
@@ -40,7 +41,117 @@ class Menu extends Model
*/
public static function getSyncFields(): array
{
return ['name', 'url', 'icon', 'sort_order', 'is_active', 'hidden', 'is_external', 'external_url'];
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 parent()

View File

@@ -0,0 +1,520 @@
<?php
namespace Database\Seeders;
use App\Models\Commons\Menu;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
/**
* MNG 메뉴 시더
*
* 현재 sidebar.blade.php의 실제 메뉴 구조를 기반으로 생성합니다.
* tenant_id=1 (기본 테넌트)용 메뉴를 생성합니다.
*
* 사용법:
* php artisan db:seed --class=MngMenuSeeder
*/
class MngMenuSeeder extends Seeder
{
protected int $tenantId = 1;
public function run(): void
{
$this->command->info('MNG 메뉴 시딩 시작...');
DB::transaction(function () {
// 기존 메뉴 삭제 (tenant_id=1)
Menu::withoutGlobalScopes()
->where('tenant_id', $this->tenantId)
->forceDelete();
$this->seedMainMenus();
$this->seedLabsMenus();
$this->seedDevToolsMenus();
});
$count = Menu::withoutGlobalScopes()->where('tenant_id', $this->tenantId)->count();
$this->command->info("MNG 메뉴 시딩 완료! (총 {$count}개)");
}
protected function seedMainMenus(): void
{
$sortOrder = 0;
// ========================================
// 대시보드
// ========================================
$this->createMenu([
'name' => '대시보드',
'url' => '/dashboard',
'icon' => 'home',
'sort_order' => $sortOrder++,
'options' => [
'route_name' => 'dashboard',
'section' => 'main',
],
]);
// ========================================
// 프로젝트 관리 그룹
// ========================================
$pmGroup = $this->createMenu([
'name' => '프로젝트 관리',
'url' => '#',
'icon' => 'folder',
'sort_order' => $sortOrder++,
'options' => [
'section' => 'main',
'meta' => ['group_id' => 'pm-group'],
],
]);
$pmSubOrder = 0;
$this->createMenu([
'parent_id' => $pmGroup->id,
'name' => '프로젝트 대시보드',
'url' => '/pm',
'icon' => 'chart-bar',
'sort_order' => $pmSubOrder++,
'options' => ['route_name' => 'pm.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $pmGroup->id,
'name' => '프로젝트',
'url' => '/pm/projects',
'icon' => 'folder',
'sort_order' => $pmSubOrder++,
'options' => ['route_name' => 'pm.projects.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $pmGroup->id,
'name' => '일일 스크럼',
'url' => '/daily-logs',
'icon' => 'calendar',
'sort_order' => $pmSubOrder++,
'options' => ['route_name' => 'daily-logs.index', 'section' => 'main'],
]);
// ========================================
// 시스템 관리 그룹
// ========================================
$systemGroup = $this->createMenu([
'name' => '시스템 관리',
'url' => '#',
'icon' => 'cog',
'sort_order' => $sortOrder++,
'options' => [
'section' => 'main',
'meta' => ['group_id' => 'system-group'],
],
]);
$systemSubOrder = 0;
$this->createMenu([
'parent_id' => $systemGroup->id,
'name' => '테넌트 관리',
'url' => '/tenants',
'icon' => 'building',
'sort_order' => $systemSubOrder++,
'options' => ['route_name' => 'tenants.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $systemGroup->id,
'name' => '사용자 관리',
'url' => '/users',
'icon' => 'users',
'sort_order' => $systemSubOrder++,
'options' => ['route_name' => 'users.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $systemGroup->id,
'name' => '부서 관리',
'url' => '/departments',
'icon' => 'building',
'sort_order' => $systemSubOrder++,
'options' => ['route_name' => 'departments.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $systemGroup->id,
'name' => '메뉴 관리',
'url' => '/menus',
'icon' => 'menu',
'sort_order' => $systemSubOrder++,
'options' => ['route_name' => 'menus.index', 'section' => 'main'],
]);
// ========================================
// 권한 관리 그룹
// ========================================
$permGroup = $this->createMenu([
'name' => '권한 관리',
'url' => '#',
'icon' => 'shield',
'sort_order' => $sortOrder++,
'options' => [
'section' => 'main',
'meta' => ['group_id' => 'permission-group'],
],
]);
$permSubOrder = 0;
$this->createMenu([
'parent_id' => $permGroup->id,
'name' => '역할 관리',
'url' => '/roles',
'icon' => 'shield-check',
'sort_order' => $permSubOrder++,
'options' => ['route_name' => 'roles.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $permGroup->id,
'name' => '권한 관리',
'url' => '/permissions',
'icon' => 'lock',
'sort_order' => $permSubOrder++,
'options' => ['route_name' => 'permissions.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $permGroup->id,
'name' => '역할 권한 관리',
'url' => '/role-permissions',
'icon' => 'shield-check',
'sort_order' => $permSubOrder++,
'options' => ['route_name' => 'role-permissions.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $permGroup->id,
'name' => '부서 권한 관리',
'url' => '/department-permissions',
'icon' => 'building',
'sort_order' => $permSubOrder++,
'options' => ['route_name' => 'department-permissions.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $permGroup->id,
'name' => '개인 권한 관리',
'url' => '/user-permissions',
'icon' => 'user',
'sort_order' => $permSubOrder++,
'options' => ['route_name' => 'user-permissions.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $permGroup->id,
'name' => '권한 분석',
'url' => '/permission-analyze',
'icon' => 'chart-bar',
'sort_order' => $permSubOrder++,
'options' => ['route_name' => 'permission-analyze.index', 'section' => 'main'],
]);
// ========================================
// 생산 관리 그룹
// ========================================
$productionGroup = $this->createMenu([
'name' => '생산 관리',
'url' => '#',
'icon' => 'cube',
'sort_order' => $sortOrder++,
'options' => [
'section' => 'main',
'meta' => ['group_id' => 'production-group'],
],
]);
$prodSubOrder = 0;
$this->createMenu([
'parent_id' => $productionGroup->id,
'name' => '품목기준 필드 관리',
'url' => '/item-fields',
'icon' => 'list',
'sort_order' => $prodSubOrder++,
'options' => ['route_name' => 'item-fields.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $productionGroup->id,
'name' => '견적수식 관리',
'url' => '/quote-formulas',
'icon' => 'calculator',
'sort_order' => $prodSubOrder++,
'options' => ['route_name' => 'quote-formulas.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $productionGroup->id,
'name' => '제품 관리',
'url' => '/products',
'icon' => 'cube',
'sort_order' => $prodSubOrder++,
'hidden' => true,
'options' => ['route_name' => 'products.index', 'section' => 'main', 'meta' => ['status' => 'preparing']],
]);
$this->createMenu([
'parent_id' => $productionGroup->id,
'name' => '자재 관리',
'url' => '/materials',
'icon' => 'archive',
'sort_order' => $prodSubOrder++,
'hidden' => true,
'options' => ['route_name' => 'materials.index', 'section' => 'main', 'meta' => ['status' => 'preparing']],
]);
$this->createMenu([
'parent_id' => $productionGroup->id,
'name' => 'BOM 관리',
'url' => '/bom',
'icon' => 'clipboard-list',
'sort_order' => $prodSubOrder++,
'hidden' => true,
'options' => ['route_name' => 'bom.index', 'section' => 'main', 'meta' => ['status' => 'preparing']],
]);
$this->createMenu([
'parent_id' => $productionGroup->id,
'name' => '카테고리 관리',
'url' => '/categories',
'icon' => 'tag',
'sort_order' => $prodSubOrder++,
'hidden' => true,
'options' => ['route_name' => 'categories.index', 'section' => 'main', 'meta' => ['status' => 'preparing']],
]);
// ========================================
// 콘텐츠 관리 그룹
// ========================================
$contentGroup = $this->createMenu([
'name' => '콘텐츠 관리',
'url' => '#',
'icon' => 'document-text',
'sort_order' => $sortOrder++,
'options' => [
'section' => 'main',
'meta' => ['group_id' => 'content-group'],
],
]);
$this->createMenu([
'parent_id' => $contentGroup->id,
'name' => '게시판 관리',
'url' => '/boards',
'icon' => 'clipboard',
'sort_order' => 0,
'options' => ['route_name' => 'boards.index', 'section' => 'main'],
]);
// ========================================
// 시스템 그룹
// ========================================
$settingsGroup = $this->createMenu([
'name' => '시스템',
'url' => '#',
'icon' => 'cog',
'sort_order' => $sortOrder++,
'options' => [
'section' => 'main',
'meta' => ['group_id' => 'system-settings-group'],
],
]);
$settingsSubOrder = 0;
$this->createMenu([
'parent_id' => $settingsGroup->id,
'name' => '시스템 설정',
'url' => '/settings',
'icon' => 'cog',
'sort_order' => $settingsSubOrder++,
'hidden' => true,
'options' => ['route_name' => 'settings.index', 'section' => 'main', 'meta' => ['status' => 'preparing']],
]);
$this->createMenu([
'parent_id' => $settingsGroup->id,
'name' => '감사 로그',
'url' => '/audit-logs',
'icon' => 'document-text',
'sort_order' => $settingsSubOrder++,
'hidden' => true,
'options' => ['route_name' => 'audit-logs.index', 'section' => 'main', 'meta' => ['status' => 'preparing']],
]);
$this->createMenu([
'parent_id' => $settingsGroup->id,
'name' => '삭제된 데이터 백업',
'url' => '/archived-records',
'icon' => 'database',
'sort_order' => $settingsSubOrder++,
'options' => ['route_name' => 'archived-records.index', 'section' => 'main'],
]);
}
protected function seedLabsMenus(): void
{
// ========================================
// R&D Labs 그룹 (S/A/M 탭 구조)
// ========================================
$labsGroup = $this->createMenu([
'name' => 'R&D Labs',
'url' => '#',
'icon' => 'beaker',
'sort_order' => 100,
'options' => [
'section' => 'labs',
'menu_type' => 'lab',
'meta' => [
'group_id' => 'lab-group',
'tabs' => ['S', 'A', 'M'],
],
],
]);
// S 탭 메뉴들 (Strategy)
$sMenus = [
['name' => '세무 전략', 'url' => '/lab/strategy/tax', 'route' => 'lab.strategy.tax'],
['name' => '노무 전략', 'url' => '/lab/strategy/labor', 'route' => 'lab.strategy.labor'],
['name' => '채권추심 전략', 'url' => '/lab/strategy/debt', 'route' => 'lab.strategy.debt'],
['name' => '스테이블코인 보고서', 'url' => '/lab/strategy/stablecoin', 'route' => 'lab.strategy.stablecoin'],
['name' => 'MRP 해외사례', 'url' => '/lab/strategy/mrp-overseas', 'route' => 'lab.strategy.mrp-overseas'],
['name' => '상담용 챗봇 전략', 'url' => '/lab/strategy/chatbot', 'route' => 'lab.strategy.chatbot'],
['name' => 'KoDATA vs NICE API', 'url' => '/lab/strategy/kodata-vs-nice', 'route' => 'lab.strategy.kodata-vs-nice'],
['name' => '바로빌 vs 팝빌 API', 'url' => '/lab/strategy/barobill-vs-popbill', 'route' => 'lab.strategy.barobill-vs-popbill'],
['name' => '사내 지식 검색 시스템', 'url' => '/lab/strategy/knowledge-search', 'route' => 'lab.strategy.knowledge-search'],
['name' => '챗봇 솔루션 비교 분석', 'url' => '/lab/strategy/chatbot-compare', 'route' => 'lab.strategy.chatbot-compare'],
['name' => 'RAG 스타트업 현황', 'url' => '/lab/strategy/rag-startups', 'route' => 'lab.strategy.rag-startups'],
['name' => '더존비즈온 분석', 'url' => '/lab/strategy/douzone', 'route' => 'lab.strategy.douzone'],
['name' => 'Confluence vs Notion', 'url' => '/lab/strategy/confluence-vs-notion', 'route' => 'lab.strategy.confluence-vs-notion'],
['name' => '차세대 QA 솔루션', 'url' => '/lab/strategy/qa-solution', 'route' => 'lab.strategy.qa-solution'],
['name' => 'SAM 영업전략', 'url' => '/lab/strategy/sales-strategy', 'route' => 'lab.strategy.sales-strategy'],
];
foreach ($sMenus as $i => $menu) {
$this->createMenu([
'parent_id' => $labsGroup->id,
'name' => $menu['name'],
'url' => $menu['url'],
'icon' => 'document',
'sort_order' => $i,
'options' => [
'route_name' => $menu['route'],
'section' => 'labs',
'menu_type' => 'lab',
'meta' => ['tab' => 'S'],
],
]);
}
// A 탭 메뉴들 (AI/Automation)
$aMenus = [
['name' => '사업자등록증 OCR', 'url' => '/lab/ai/business-ocr', 'route' => 'lab.ai.business-ocr'],
['name' => '웹 녹음 AI 요약', 'url' => '/lab/ai/web-recording', 'route' => 'lab.ai.web-recording'],
['name' => '회의록 AI 요약', 'url' => '/lab/ai/meeting-summary', 'route' => 'lab.ai.meeting-summary'],
['name' => '업무협의록 AI 요약', 'url' => '/lab/ai/work-memo-summary', 'route' => 'lab.ai.work-memo-summary'],
['name' => '운영자용 챗봇', 'url' => '/lab/ai/operator-chatbot', 'route' => 'lab.ai.operator-chatbot'],
['name' => 'Vertex RAG 챗봇', 'url' => '/lab/ai/vertex-rag', 'route' => 'lab.ai.vertex-rag'],
['name' => '테넌트 지식 업로드', 'url' => '/lab/ai/tenant-knowledge', 'route' => 'lab.ai.tenant-knowledge'],
['name' => '테넌트 챗봇', 'url' => '/lab/ai/tenant-chatbot', 'route' => 'lab.ai.tenant-chatbot'],
['name' => 'SAM AI 메뉴 이동', 'url' => '/lab/ai/sam-ai-menu', 'route' => 'lab.ai.sam-ai-menu'],
['name' => 'SAM AI 알람음 제작', 'url' => '/lab/ai/sam-ai-alarm', 'route' => 'lab.ai.sam-ai-alarm'],
['name' => 'GPS 출퇴근 관리', 'url' => '/lab/ai/gps-attendance', 'route' => 'lab.ai.gps-attendance'],
['name' => '기업개황 조회', 'url' => '/lab/ai/company-overview', 'route' => 'lab.ai.company-overview'],
];
foreach ($aMenus as $i => $menu) {
$this->createMenu([
'parent_id' => $labsGroup->id,
'name' => $menu['name'],
'url' => $menu['url'],
'icon' => 'chip',
'sort_order' => 100 + $i,
'options' => [
'route_name' => $menu['route'],
'section' => 'labs',
'menu_type' => 'lab',
'meta' => ['tab' => 'A'],
],
]);
}
// M 탭 메뉴들 (Management)
$mMenus = [
['name' => '바로빌 테넌트 관리', 'url' => '/lab/management/barobill-tenant', 'route' => 'lab.management.barobill-tenant'],
['name' => '전자세금계산서 전략', 'url' => '/lab/management/tax-invoice-strategy', 'route' => 'lab.management.tax-invoice-strategy'],
['name' => '전자세금계산서', 'url' => '/lab/management/tax-invoice', 'route' => 'lab.management.tax-invoice'],
['name' => '사업자등록번호 진위 확인', 'url' => '/lab/management/business-verify', 'route' => 'lab.management.business-verify'],
['name' => '영업관리 & 매니저 미팅관리', 'url' => '/lab/management/sales-meeting', 'route' => 'lab.management.sales-meeting'],
['name' => '카드 세무항목 매칭 전략', 'url' => '/lab/management/card-tax-matching', 'route' => 'lab.management.card-tax-matching'],
['name' => '한국 카드사 API 보고서', 'url' => '/lab/management/card-api-report', 'route' => 'lab.management.card-api-report'],
['name' => '카드 사용내역 수집 후 매칭', 'url' => '/lab/management/card-usage-matching', 'route' => 'lab.management.card-usage-matching'],
['name' => '계좌입출금 내역 조회 API', 'url' => '/lab/management/account-api', 'route' => 'lab.management.account-api'],
['name' => '영업관리 시나리오', 'url' => '/lab/management/sales-scenario', 'route' => 'lab.management.sales-scenario'],
['name' => '매니저 시나리오', 'url' => '/lab/management/manager-scenario', 'route' => 'lab.management.manager-scenario'],
];
foreach ($mMenus as $i => $menu) {
$this->createMenu([
'parent_id' => $labsGroup->id,
'name' => $menu['name'],
'url' => $menu['url'],
'icon' => 'briefcase',
'sort_order' => 200 + $i,
'options' => [
'route_name' => $menu['route'],
'section' => 'labs',
'menu_type' => 'lab',
'meta' => ['tab' => 'M'],
],
]);
}
}
protected function seedDevToolsMenus(): void
{
// ========================================
// 개발 도구 그룹 (하단 고정)
// ========================================
$devToolsGroup = $this->createMenu([
'name' => '개발 도구',
'url' => '#',
'icon' => 'code',
'sort_order' => 900,
'options' => [
'section' => 'tools',
'menu_type' => 'tool',
'meta' => ['group_id' => 'dev-tools-group', 'position' => 'bottom'],
],
]);
$devSubOrder = 0;
$this->createMenu([
'parent_id' => $devToolsGroup->id,
'name' => 'API 플로우 테스터',
'url' => '/dev-tools/flow-tester',
'icon' => 'terminal',
'sort_order' => $devSubOrder++,
'options' => [
'route_name' => 'dev-tools.flow-tester.index',
'section' => 'tools',
'menu_type' => 'tool',
],
]);
$this->createMenu([
'parent_id' => $devToolsGroup->id,
'name' => 'API 요청 로그',
'url' => '/dev-tools/api-logs',
'icon' => 'document-text',
'sort_order' => $devSubOrder++,
'options' => [
'route_name' => 'dev-tools.api-logs.index',
'section' => 'tools',
'menu_type' => 'tool',
],
]);
}
protected function createMenu(array $data): Menu
{
$defaults = [
'tenant_id' => $this->tenantId,
'is_active' => true,
'hidden' => false,
'is_external' => false,
'is_customized' => false,
];
return Menu::create(array_merge($defaults, $data));
}
}