- QuoteApi: 견적 API Swagger 문서 추가 - Board 모델, GlobalMenuService, MenuSyncService 오류 수정
271 lines
7.5 KiB
PHP
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();
|
|
}
|
|
}
|