feat: 글로벌 메뉴 템플릿 복제 시스템 구현
- GlobalMenuTemplateSeeder 추가: 60개 메뉴 템플릿 생성 (tenant_id=NULL) - MenuBootstrapService.cloneGlobalMenusForTenant() 추가 - parent_id 매핑으로 계층 구조 유지 - DB 트랜잭션으로 원자성 보장 - RegisterService 업데이트: 신규 회원가입 시 메뉴 템플릿 복제 - 기존 225개 테넌트에 메뉴 일괄 복제 완료 - 테스트 완료: 회원가입 + 로그인 시 60개 메뉴 정상 반환
This commit is contained in:
@@ -11,11 +11,58 @@
|
||||
class MenuBootstrapService
|
||||
{
|
||||
/**
|
||||
* 테넌트를 위한 기본 메뉴 구조 생성
|
||||
* 글로벌 메뉴 템플릿을 테넌트에 복제
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @return array 생성된 메뉴 ID 목록
|
||||
*/
|
||||
public static function cloneGlobalMenusForTenant(int $tenantId): array
|
||||
{
|
||||
return DB::transaction(function () use ($tenantId) {
|
||||
// 1. 글로벌 템플릿 메뉴 조회 (parent_id 순서대로 정렬)
|
||||
$templateMenus = Menu::withoutGlobalScopes()
|
||||
->whereNull('tenant_id')
|
||||
->orderByRaw('COALESCE(parent_id, 0)')
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
// 2. parent_id 매핑 테이블 생성
|
||||
$idMapping = [];
|
||||
$menuIds = [];
|
||||
|
||||
// 3. 계층 순서대로 복제 (parent → child)
|
||||
foreach ($templateMenus as $template) {
|
||||
$newMenu = Menu::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'parent_id' => $template->parent_id
|
||||
? ($idMapping[$template->parent_id] ?? null)
|
||||
: null,
|
||||
'name' => $template->name,
|
||||
'url' => $template->url,
|
||||
'icon' => $template->icon,
|
||||
'sort_order' => $template->sort_order,
|
||||
'is_active' => $template->is_active,
|
||||
'hidden' => $template->hidden,
|
||||
'is_external' => $template->is_external,
|
||||
'external_url' => $template->external_url,
|
||||
]);
|
||||
|
||||
// 4. ID 매핑 저장
|
||||
$idMapping[$template->id] = $newMenu->id;
|
||||
$menuIds[] = $newMenu->id;
|
||||
}
|
||||
|
||||
return $menuIds;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트를 위한 기본 메뉴 구조 생성 (구버전 - 하위 호환성 유지)
|
||||
*
|
||||
* @deprecated Use cloneGlobalMenusForTenant() instead
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @return array 생성된 메뉴 ID 목록
|
||||
*/
|
||||
public static function createDefaultMenus(int $tenantId): array
|
||||
{
|
||||
return DB::transaction(function () use ($tenantId) {
|
||||
|
||||
@@ -51,8 +51,8 @@ public static function register(array $params): array
|
||||
],
|
||||
]);
|
||||
|
||||
// 3. Create default menus for tenant
|
||||
$menuIds = MenuBootstrapService::createDefaultMenus($tenant->id);
|
||||
// 3. Clone global menu template for tenant
|
||||
$menuIds = MenuBootstrapService::cloneGlobalMenusForTenant($tenant->id);
|
||||
|
||||
// 4. Create User with hashed password and options
|
||||
$user = User::create([
|
||||
|
||||
@@ -20,5 +20,10 @@ public function run(): void
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
// 글로벌 메뉴 템플릿 시딩
|
||||
$this->call([
|
||||
GlobalMenuTemplateSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
271
database/seeders/GlobalMenuTemplateSeeder.php
Normal file
271
database/seeders/GlobalMenuTemplateSeeder.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Commons\Menu;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GlobalMenuTemplateSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 글로벌 메뉴 템플릿 시딩 (tenant_id = NULL)
|
||||
* mes_react App.tsx의 SystemAdmin 메뉴 구조 기반
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
// 아이콘 매핑 (React icon → 문자열 표현)
|
||||
$iconMap = [
|
||||
'LayoutDashboard' => 'layout-dashboard',
|
||||
'Users' => 'users',
|
||||
'Sliders' => 'sliders',
|
||||
'Database' => 'database',
|
||||
'Box' => 'box',
|
||||
'Archive' => 'archive',
|
||||
'Wrench' => 'wrench',
|
||||
'Building' => 'building',
|
||||
'User' => 'user',
|
||||
'ClipboardCheck' => 'clipboard-check',
|
||||
'Code' => 'code',
|
||||
'Calculator' => 'calculator',
|
||||
'Palette' => 'palette',
|
||||
'TrendingUp' => 'trending-up',
|
||||
'Building2' => 'building-2',
|
||||
'BarChart3' => 'bar-chart-3',
|
||||
'Briefcase' => 'briefcase',
|
||||
'ShoppingCart' => 'shopping-cart',
|
||||
'MapPin' => 'map-pin',
|
||||
'DollarSign' => 'dollar-sign',
|
||||
'FileText' => 'file-text',
|
||||
'Factory' => 'factory',
|
||||
'Package' => 'package',
|
||||
'CheckSquare' => 'check-square',
|
||||
'CheckCircle' => 'check-circle',
|
||||
'Warehouse' => 'warehouse',
|
||||
'Layers' => 'layers',
|
||||
'Truck' => 'truck',
|
||||
'XCircle' => 'x-circle',
|
||||
'Car' => 'car',
|
||||
'Shield' => 'shield',
|
||||
'Settings' => 'settings',
|
||||
'Activity' => 'activity',
|
||||
'Server' => 'server',
|
||||
];
|
||||
|
||||
// SystemAdmin 메뉴 구조 정의
|
||||
$menus = [
|
||||
[
|
||||
'name' => '시스템 대시보드',
|
||||
'url' => '/dashboard',
|
||||
'icon' => $iconMap['LayoutDashboard'],
|
||||
'sort_order' => 1,
|
||||
],
|
||||
[
|
||||
'name' => '사용자 관리',
|
||||
'url' => '/users',
|
||||
'icon' => $iconMap['Users'],
|
||||
'sort_order' => 2,
|
||||
],
|
||||
[
|
||||
'name' => '메뉴 커스터마이징',
|
||||
'url' => '/menu-customization',
|
||||
'icon' => $iconMap['Sliders'],
|
||||
'sort_order' => 3,
|
||||
],
|
||||
[
|
||||
'name' => '기준정보 관리',
|
||||
'url' => '/master-data',
|
||||
'icon' => $iconMap['Database'],
|
||||
'sort_order' => 4,
|
||||
'children' => [
|
||||
['name' => '모델관리', 'url' => '/master-data/models-management', 'icon' => $iconMap['Box']],
|
||||
['name' => '자재관리', 'url' => '/master-data/items-management', 'icon' => $iconMap['Archive']],
|
||||
['name' => '품목기준관리', 'url' => '/master-data/item-master-data-management', 'icon' => $iconMap['Database']],
|
||||
['name' => '공정관리', 'url' => '/master-data/process-management', 'icon' => $iconMap['Wrench']],
|
||||
['name' => '거래처관리', 'url' => '/master-data/customer-management', 'icon' => $iconMap['Building']],
|
||||
['name' => '사원관리', 'url' => '/master-data/employee-management', 'icon' => $iconMap['User']],
|
||||
['name' => '부서관리', 'url' => '/master-data/department-management', 'icon' => $iconMap['Building']],
|
||||
['name' => '검사기준관리', 'url' => '/master-data/inspection-standard', 'icon' => $iconMap['ClipboardCheck']],
|
||||
['name' => '코드관리', 'url' => '/master-data/code-management', 'icon' => $iconMap['Code']],
|
||||
['name' => '견적수식관리', 'url' => '/master-data/formula-management', 'icon' => $iconMap['Calculator']],
|
||||
['name' => '디자인시스템', 'url' => '/master-data/design-system', 'icon' => $iconMap['Palette']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '영업관리',
|
||||
'url' => '/sales-dept',
|
||||
'icon' => $iconMap['TrendingUp'],
|
||||
'sort_order' => 5,
|
||||
'children' => [
|
||||
['name' => '리드관리', 'url' => '/sales-dept/sales-leads', 'icon' => $iconMap['Users']],
|
||||
['name' => '매출처관리', 'url' => '/sales-dept/customer-account-management', 'icon' => $iconMap['Building2']],
|
||||
['name' => '영업실적', 'url' => '/sales-dept/sales-performance', 'icon' => $iconMap['BarChart3']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '판매관리',
|
||||
'url' => '/sales',
|
||||
'icon' => $iconMap['Briefcase'],
|
||||
'sort_order' => 6,
|
||||
'children' => [
|
||||
['name' => '거래처관리', 'url' => '/sales/client-management-sales-admin', 'icon' => $iconMap['Building2']],
|
||||
['name' => '견적관리', 'url' => '/sales/quote-management', 'icon' => $iconMap['Calculator']],
|
||||
['name' => '수주관리', 'url' => '/sales/order-management-sales', 'icon' => $iconMap['ShoppingCart']],
|
||||
['name' => '현장관리', 'url' => '/sales/site-management', 'icon' => $iconMap['MapPin']],
|
||||
['name' => '단가관리', 'url' => '/sales/pricing-management', 'icon' => $iconMap['DollarSign']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '구매관리',
|
||||
'url' => '/purchase',
|
||||
'icon' => $iconMap['ShoppingCart'],
|
||||
'sort_order' => 7,
|
||||
'children' => [
|
||||
['name' => '거래처관리', 'url' => '/purchase/supplier-management', 'icon' => $iconMap['Building2']],
|
||||
['name' => '발주관리', 'url' => '/purchase/purchase-order', 'icon' => $iconMap['FileText']],
|
||||
['name' => '구매현황', 'url' => '/purchase/purchase-status', 'icon' => $iconMap['BarChart3']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '생산관리',
|
||||
'url' => '/production',
|
||||
'icon' => $iconMap['Factory'],
|
||||
'sort_order' => 8,
|
||||
'children' => [
|
||||
['name' => '스크린 생산', 'url' => '/production/screen-production', 'icon' => $iconMap['Package']],
|
||||
['name' => '슬랫 생산', 'url' => '/production/slat-production', 'icon' => $iconMap['Package']],
|
||||
['name' => '절곡 생산', 'url' => '/production/bending-production', 'icon' => $iconMap['Package']],
|
||||
['name' => '재고 생산', 'url' => '/production/stock-production', 'icon' => $iconMap['Package']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '품질관리',
|
||||
'url' => '/quality',
|
||||
'icon' => $iconMap['ClipboardCheck'],
|
||||
'sort_order' => 9,
|
||||
'children' => [
|
||||
['name' => '수입검사', 'url' => '/quality/incoming-inspection', 'icon' => $iconMap['CheckSquare']],
|
||||
['name' => '중간검사', 'url' => '/quality/process-inspection', 'icon' => $iconMap['ClipboardCheck']],
|
||||
['name' => '제품검사', 'url' => '/quality/final-inspection', 'icon' => $iconMap['CheckCircle']],
|
||||
['name' => '품목별 검사', 'url' => '/quality/quality-item-inspection', 'icon' => $iconMap['Package']],
|
||||
['name' => '품목별 검사 (고급)', 'url' => '/quality/quality-item-inspection-enhanced', 'icon' => $iconMap['BarChart3']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '자재관리',
|
||||
'url' => '/material',
|
||||
'icon' => $iconMap['Warehouse'],
|
||||
'sort_order' => 10,
|
||||
'children' => [
|
||||
['name' => '재고현황', 'url' => '/material/stock-status', 'icon' => $iconMap['Layers']],
|
||||
['name' => '입고관리', 'url' => '/material/receiving-management', 'icon' => $iconMap['Package']],
|
||||
['name' => '출고관리', 'url' => '/material/shipping-management', 'icon' => $iconMap['Truck']],
|
||||
['name' => '부적합품관리', 'url' => '/material/nonconforming-management', 'icon' => $iconMap['XCircle']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '장비관리',
|
||||
'url' => '/equipment',
|
||||
'icon' => $iconMap['Wrench'],
|
||||
'sort_order' => 11,
|
||||
'children' => [
|
||||
['name' => '설비관리', 'url' => '/equipment/equipment-management', 'icon' => $iconMap['Wrench']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '차량관리',
|
||||
'url' => '/vehicle',
|
||||
'icon' => $iconMap['Truck'],
|
||||
'sort_order' => 12,
|
||||
'children' => [
|
||||
['name' => '차량관리', 'url' => '/vehicle/vehicle-management', 'icon' => $iconMap['Car']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '회계관리',
|
||||
'url' => '/accounting',
|
||||
'icon' => $iconMap['Calculator'],
|
||||
'sort_order' => 13,
|
||||
'children' => [
|
||||
['name' => '거래처관리', 'url' => '/accounting/client-management', 'icon' => $iconMap['Building2']],
|
||||
['name' => '매출관리', 'url' => '/accounting/sales-accounting', 'icon' => $iconMap['TrendingUp']],
|
||||
['name' => '매입관리', 'url' => '/accounting/purchase-accounting', 'icon' => $iconMap['TrendingUp']],
|
||||
['name' => '원가관리', 'url' => '/accounting/cost-management', 'icon' => $iconMap['DollarSign']],
|
||||
['name' => '재무제표', 'url' => '/accounting/financial-statements', 'icon' => $iconMap['FileText']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '권한 관리',
|
||||
'url' => '/permissions',
|
||||
'icon' => $iconMap['Shield'],
|
||||
'sort_order' => 14,
|
||||
],
|
||||
[
|
||||
'name' => '시스템 설정',
|
||||
'url' => '/system',
|
||||
'icon' => $iconMap['Settings'],
|
||||
'sort_order' => 15,
|
||||
],
|
||||
[
|
||||
'name' => '데이터베이스',
|
||||
'url' => '/database',
|
||||
'icon' => $iconMap['Database'],
|
||||
'sort_order' => 16,
|
||||
],
|
||||
[
|
||||
'name' => '시스템 모니터링',
|
||||
'url' => '/monitoring',
|
||||
'icon' => $iconMap['Activity'],
|
||||
'sort_order' => 17,
|
||||
],
|
||||
[
|
||||
'name' => '보안 관리',
|
||||
'url' => '/security',
|
||||
'icon' => $iconMap['Server'],
|
||||
'sort_order' => 18,
|
||||
],
|
||||
];
|
||||
|
||||
// 메뉴 생성
|
||||
foreach ($menus as $menuData) {
|
||||
$children = $menuData['children'] ?? null;
|
||||
unset($menuData['children']);
|
||||
|
||||
// 최상위 메뉴 생성 (tenant_id = null)
|
||||
$parentMenu = Menu::create([
|
||||
'tenant_id' => null,
|
||||
'parent_id' => null,
|
||||
'name' => $menuData['name'],
|
||||
'url' => $menuData['url'],
|
||||
'icon' => $menuData['icon'],
|
||||
'sort_order' => $menuData['sort_order'],
|
||||
'is_active' => 1,
|
||||
'hidden' => 0,
|
||||
'is_external' => 0,
|
||||
'external_url' => null,
|
||||
]);
|
||||
|
||||
// 하위 메뉴 생성
|
||||
if ($children) {
|
||||
foreach ($children as $index => $childData) {
|
||||
Menu::create([
|
||||
'tenant_id' => null,
|
||||
'parent_id' => $parentMenu->id,
|
||||
'name' => $childData['name'],
|
||||
'url' => $childData['url'],
|
||||
'icon' => $childData['icon'],
|
||||
'sort_order' => $index + 1,
|
||||
'is_active' => 1,
|
||||
'hidden' => 0,
|
||||
'is_external' => 0,
|
||||
'external_url' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->command->info('✅ 글로벌 메뉴 템플릿 생성 완료 (약 60개 메뉴 항목)');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user