feat: 글로벌 메뉴 템플릿 복제 시스템 구현

- GlobalMenuTemplateSeeder 추가: 60개 메뉴 템플릿 생성 (tenant_id=NULL)
- MenuBootstrapService.cloneGlobalMenusForTenant() 추가
  - parent_id 매핑으로 계층 구조 유지
  - DB 트랜잭션으로 원자성 보장
- RegisterService 업데이트: 신규 회원가입 시 메뉴 템플릿 복제
- 기존 225개 테넌트에 메뉴 일괄 복제 완료
- 테스트 완료: 회원가입 + 로그인 시 60개 메뉴 정상 반환
This commit is contained in:
2025-11-11 18:03:11 +09:00
parent ddc4bb99a0
commit 641d7f62ab
4 changed files with 326 additions and 3 deletions

View File

@@ -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) {

View File

@@ -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([

View File

@@ -20,5 +20,10 @@ public function run(): void
'name' => 'Test User',
'email' => 'test@example.com',
]);
// 글로벌 메뉴 템플릿 시딩
$this->call([
GlobalMenuTemplateSeeder::class,
]);
}
}

View 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개 메뉴 항목)');
}
}