Files
sam-api/app/Services/GlobalMenuService.php
hskwon c4a4f85e63 feat: QuoteApi Swagger 추가 및 서비스 수정
- QuoteApi: 견적 API Swagger 문서 추가
- Board 모델, GlobalMenuService, MenuSyncService 오류 수정
2025-12-09 09:39:41 +09:00

271 lines
7.5 KiB
PHP

<?php
namespace App\Services;
use App\Models\Commons\Menu;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
/**
* 글로벌 메뉴 관리 서비스 (시스템 관리자용)
*/
class GlobalMenuService extends Service
{
/**
* 글로벌 메뉴 목록 조회
*/
public function index(array $params = []): Collection
{
$query = Menu::global();
// 필터: 활성 상태
if (isset($params['is_active'])) {
$query->where('is_active', (bool) $params['is_active']);
}
// 필터: 숨김 상태
if (isset($params['hidden'])) {
$query->where('hidden', (bool) $params['hidden']);
}
// 필터: 상위 메뉴
if (array_key_exists('parent_id', $params)) {
$query->where('parent_id', $params['parent_id']);
}
return $query
->orderByRaw('COALESCE(parent_id, 0)')
->orderBy('sort_order')
->get();
}
/**
* 글로벌 메뉴 트리 구조로 조회
*/
public function tree(): Collection
{
$menus = Menu::global()
->orderBy('sort_order')
->get();
return $this->buildTree($menus);
}
/**
* 글로벌 메뉴 단건 조회
*/
public function show(int $id): ?Menu
{
return Menu::global()
->with('children')
->find($id);
}
/**
* 글로벌 메뉴 생성
*/
public function store(array $data): Menu
{
return Menu::create([
'tenant_id' => null, // 글로벌 메뉴
'parent_id' => $data['parent_id'] ?? null,
'name' => $data['name'],
'url' => $data['url'] ?? null,
'icon' => $data['icon'] ?? null,
'sort_order' => $data['sort_order'] ?? 0,
'is_active' => $data['is_active'] ?? true,
'hidden' => $data['hidden'] ?? false,
'is_external' => $data['is_external'] ?? false,
'external_url' => $data['external_url'] ?? null,
'created_by' => $this->apiUserId(),
'updated_by' => $this->apiUserId(),
]);
}
/**
* 글로벌 메뉴 수정
*/
public function update(int $id, array $data): ?Menu
{
$menu = Menu::global()->find($id);
if (! $menu) {
return null;
}
$updateData = array_filter([
'parent_id' => $data['parent_id'] ?? null,
'name' => $data['name'] ?? null,
'url' => $data['url'] ?? null,
'icon' => $data['icon'] ?? null,
'sort_order' => $data['sort_order'] ?? null,
'is_active' => isset($data['is_active']) ? (bool) $data['is_active'] : null,
'hidden' => isset($data['hidden']) ? (bool) $data['hidden'] : null,
'is_external' => isset($data['is_external']) ? (bool) $data['is_external'] : null,
'external_url' => $data['external_url'] ?? null,
], fn ($v) => ! is_null($v));
$updateData['updated_by'] = $this->apiUserId();
$menu->update($updateData);
return $menu->fresh();
}
/**
* 글로벌 메뉴 삭제
* - 연결된 테넌트 메뉴의 global_menu_id를 NULL로 변경
*/
public function destroy(int $id): bool
{
$menu = Menu::global()->find($id);
if (! $menu) {
return false;
}
return DB::transaction(function () use ($menu) {
// 연결된 테넌트 메뉴의 global_menu_id를 NULL로 변경
Menu::withoutGlobalScopes()
->where('global_menu_id', $menu->id)
->update(['global_menu_id' => null]);
// 하위 메뉴도 삭제
Menu::global()
->where('parent_id', $menu->id)
->delete();
$menu->deleted_by = $this->apiUserId();
$menu->save();
$menu->delete();
return true;
});
}
/**
* 글로벌 메뉴 순서 변경
*/
public function reorder(array $items): bool
{
return DB::transaction(function () use ($items) {
foreach ($items as $item) {
if (! isset($item['id'], $item['sort_order'])) {
continue;
}
Menu::global()
->where('id', $item['id'])
->update([
'sort_order' => (int) $item['sort_order'],
'updated_by' => $this->apiUserId(),
]);
}
return true;
});
}
/**
* 특정 글로벌 메뉴를 모든 테넌트에 동기화
* - 이미 복제된 테넌트는 건너뜀
*/
public function syncToAllTenants(int $globalMenuId): array
{
$globalMenu = Menu::global()->find($globalMenuId);
if (! $globalMenu) {
return ['synced' => 0, 'skipped' => 0, 'details' => []];
}
// 모든 테넌트 ID 조회
$tenantIds = Menu::withoutGlobalScopes()
->whereNotNull('tenant_id')
->distinct()
->pluck('tenant_id');
$synced = 0;
$skipped = 0;
$details = [];
foreach ($tenantIds as $tenantId) {
// 이미 복제된 경우 건너뜀
$exists = Menu::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('global_menu_id', $globalMenuId)
->exists();
if ($exists) {
$skipped++;
$details[] = [
'tenant_id' => $tenantId,
'action' => 'skipped',
'reason' => 'already_exists',
];
continue;
}
// 복제
$newMenu = MenuBootstrapService::cloneSingleMenu($globalMenuId, $tenantId);
if ($newMenu) {
$synced++;
$details[] = [
'tenant_id' => $tenantId,
'action' => 'created',
'tenant_menu_id' => $newMenu->id,
];
}
}
return [
'synced' => $synced,
'skipped' => $skipped,
'details' => $details,
];
}
/**
* 글로벌 메뉴 통계 조회
*/
public function stats(): array
{
$total = Menu::global()->count();
$active = Menu::global()->where('is_active', true)->count();
$inactive = Menu::global()->where('is_active', false)->count();
$hidden = Menu::global()->where('hidden', true)->count();
// 테넌트별 사용 현황
$tenantUsage = Menu::withoutGlobalScopes()
->whereNotNull('global_menu_id')
->groupBy('global_menu_id')
->selectRaw('global_menu_id, COUNT(DISTINCT tenant_id) as tenant_count')
->pluck('tenant_count', 'global_menu_id')
->toArray();
return [
'total' => $total,
'active' => $active,
'inactive' => $inactive,
'hidden' => $hidden,
'tenant_usage' => $tenantUsage,
];
}
/**
* 메뉴 컬렉션을 트리 구조로 변환
*/
private function buildTree(Collection $menus, ?int $parentId = null): Collection
{
return $menus
->filter(fn ($menu) => $menu->parent_id === $parentId)
->map(function ($menu) use ($menus) {
$menu->children = $this->buildTree($menus, $menu->id);
return $menu;
})
->values();
}
}