attributes->get('tenant_id'); } protected static function actorId(array $params): ?int { return $params['user_id'] ?? (request()->user()->id ?? null); } public static function index(array $params) { $tenantId = self::tenantId($params); $q = DB::table('menus')->whereNull('deleted_at'); if (!is_null($tenantId)) { $q->where(function ($w) use ($tenantId) { $w->whereNull('tenant_id')->orWhere('tenant_id', $tenantId); }); } // 옵션: parent_id / is_active / hidden 필터 if (isset($params['parent_id'])) $q->where('parent_id', $params['parent_id']); if (isset($params['is_active'])) $q->where('is_active', (int)$params['is_active']); if (isset($params['hidden'])) $q->where('hidden', (int)$params['hidden']); return $q->orderBy('parent_id')->orderBy('sort_order')->get(); } public static function show(array $params) { $id = (int)($params['id'] ?? 0); $tenantId = self::tenantId($params); $q = DB::table('menus')->where('id', $id)->whereNull('deleted_at'); if (!is_null($tenantId)) { $q->where(function ($w) use ($tenantId) { $w->whereNull('tenant_id')->orWhere('tenant_id', $tenantId); }); } $row = $q->first(); throw_if(!$row, ValidationException::withMessages(['id' => 'Menu not found'])); return $row; } public static function store(array $params) { $tenantId = self::tenantId($params); $userId = self::actorId($params); $v = Validator::make($params, [ 'parent_id' => ['nullable','integer'], 'name' => ['required','string','max:100'], 'slug' => ['nullable','string','max:150'], 'url' => ['nullable','string','max:255'], 'is_active' => ['nullable','boolean'], 'sort_order' => ['nullable','integer'], 'hidden' => ['nullable','boolean'], 'is_external' => ['nullable','boolean'], 'external_url' => ['nullable','string','max:255'], 'icon' => ['nullable','string','max:50'], ]); $data = $v->validated(); // slug 유니크(테넌트 범위) 체크 if (!empty($data['slug'])) { $exists = DB::table('menus') ->whereNull('deleted_at') ->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) ->where('slug',$data['slug']) ->exists(); if ($exists) { throw ValidationException::withMessages(['slug'=>'이미 사용 중인 슬러그입니다.']); } } $now = now(); $id = DB::table('menus')->insertGetId([ 'tenant_id' => $tenantId, 'parent_id' => $data['parent_id'] ?? null, 'name' => $data['name'], 'slug' => $data['slug'] ?? null, 'url' => $data['url'] ?? null, 'is_active' => (int)($data['is_active'] ?? 1), 'sort_order' => (int)($data['sort_order'] ?? 0), 'hidden' => (int)($data['hidden'] ?? 0), 'is_external' => (int)($data['is_external'] ?? 0), 'external_url' => $data['external_url'] ?? null, 'icon' => $data['icon'] ?? null, 'created_at' => $now, 'updated_at' => $now, 'created_by' => $userId, 'updated_by' => $userId, ]); return ['id' => $id]; } public static function update(array $params) { $id = (int)($params['id'] ?? 0); $tenantId = self::tenantId($params); $userId = self::actorId($params); $v = Validator::make($params, [ 'parent_id' => ['nullable','integer'], 'name' => ['nullable','string','max:100'], 'slug' => ['nullable','string','max:150'], 'url' => ['nullable','string','max:255'], 'is_active' => ['nullable','boolean'], 'sort_order' => ['nullable','integer'], 'hidden' => ['nullable','boolean'], 'is_external' => ['nullable','boolean'], 'external_url' => ['nullable','string','max:255'], 'icon' => ['nullable','string','max:50'], ]); $data = $v->validated(); // 대상 존재 확인 & 테넌트 범위 $exists = DB::table('menus')->where('id',$id)->whereNull('deleted_at') ->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) ->exists(); if (!$exists) throw ValidationException::withMessages(['id'=>'Menu not found']); // slug 유니크(테넌트 범위) 체크 if (!empty($data['slug'])) { $dup = DB::table('menus')->whereNull('deleted_at') ->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) ->where('slug',$data['slug'])->where('id','<>',$id)->exists(); if ($dup) throw ValidationException::withMessages(['slug'=>'이미 사용 중인 슬러그입니다.']); } $update = Arr::only($data, ['parent_id','name','slug','url','is_active','sort_order','hidden','is_external','external_url','icon']); $update = array_filter($update, fn($v)=>!is_null($v)); $update['updated_at'] = now(); $update['updated_by'] = $userId; DB::table('menus')->where('id',$id)->update($update); return ['id' => $id]; } public static function destroy(array $params) { $id = (int)($params['id'] ?? 0); $tenantId = self::tenantId($params); $userId = self::actorId($params); $q = DB::table('menus')->where('id',$id)->whereNull('deleted_at'); $q = !is_null($tenantId) ? $q->where('tenant_id',$tenantId) : $q->whereNull('tenant_id'); $row = $q->first(); if (!$row) throw ValidationException::withMessages(['id'=>'Menu not found']); DB::table('menus')->where('id',$id)->update([ 'deleted_at' => now(), 'deleted_by' => $userId, ]); return ['id' => $id, 'deleted' => true]; } /** * 정렬 일괄 변경 * $params = [ ['id'=>10, 'sort_order'=>1], ... ] */ public static function reorder(array $params) { if (!is_array($params) || empty($params)) { throw ValidationException::withMessages(['items'=>'유효한 정렬 목록이 필요합니다.']); } DB::transaction(function () use ($params) { foreach ($params as $it) { if (!isset($it['id'], $it['sort_order'])) continue; DB::table('menus')->where('id',(int)$it['id'])->update([ 'sort_order' => (int)$it['sort_order'], 'updated_at' => now(), ]); } }); return true; } /** * 상태 토글 * 허용 필드: is_active / hidden / is_external */ public static function toggle(array $params) { $id = (int)($params['id'] ?? 0); $userId = self::actorId($params); $payload = array_filter([ 'is_active' => isset($params['is_active']) ? (int)$params['is_active'] : null, 'hidden' => isset($params['hidden']) ? (int)$params['hidden'] : null, 'is_external' => isset($params['is_external']) ? (int)$params['is_external'] : null, ], fn($v)=>!is_null($v)); if (empty($payload)) { throw ValidationException::withMessages(['toggle'=>'변경할 필드가 없습니다.']); } $payload['updated_at'] = now(); $payload['updated_by'] = $userId; DB::table('menus')->where('id',$id)->update($payload); return ['id' => $id]; } }