390 lines
12 KiB
PHP
390 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Tenants\Tenant;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class TenantService
|
|
{
|
|
public function __construct(
|
|
private readonly ArchiveService $archiveService
|
|
) {}
|
|
|
|
/**
|
|
* 테넌트 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getTenants(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$query = Tenant::query()
|
|
->withCount(['users', 'departments', 'menus', 'roles'])
|
|
->withTrashed();
|
|
|
|
// 검색 필터
|
|
if (! empty($filters['search'])) {
|
|
$search = $filters['search'];
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('company_name', 'like', "%{$search}%")
|
|
->orWhere('code', 'like', "%{$search}%")
|
|
->orWhere('email', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 상태 필터
|
|
if (! empty($filters['tenant_st_code'])) {
|
|
$query->where('tenant_st_code', $filters['tenant_st_code']);
|
|
}
|
|
|
|
// Soft Delete 필터
|
|
if (isset($filters['trashed'])) {
|
|
if ($filters['trashed'] === 'only') {
|
|
$query->onlyTrashed();
|
|
} elseif ($filters['trashed'] === 'with') {
|
|
$query->withTrashed();
|
|
}
|
|
}
|
|
|
|
// 정렬
|
|
$sortBy = $filters['sort_by'] ?? 'id';
|
|
$sortDirection = $filters['sort_direction'] ?? 'desc';
|
|
$query->orderBy($sortBy, $sortDirection);
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
|
|
/**
|
|
* 특정 테넌트 조회
|
|
*/
|
|
public function getTenantById(int $id, bool $withTrashed = false): ?Tenant
|
|
{
|
|
$query = Tenant::query()->withCount(['users', 'departments', 'menus', 'roles']);
|
|
|
|
if ($withTrashed) {
|
|
$query->withTrashed();
|
|
}
|
|
|
|
return $query->find($id);
|
|
}
|
|
|
|
/**
|
|
* 테넌트 생성
|
|
*/
|
|
public function createTenant(array $data): Tenant
|
|
{
|
|
return Tenant::create($data);
|
|
}
|
|
|
|
/**
|
|
* 테넌트 수정
|
|
*/
|
|
public function updateTenant(int $id, array $data): bool
|
|
{
|
|
$tenant = Tenant::findOrFail($id);
|
|
|
|
return $tenant->update($data);
|
|
}
|
|
|
|
/**
|
|
* 테넌트 삭제 (Soft Delete)
|
|
*/
|
|
public function deleteTenant(int $id): bool
|
|
{
|
|
$tenant = Tenant::findOrFail($id);
|
|
|
|
// 삭제자 기록
|
|
$tenant->deleted_by = auth()->id();
|
|
$tenant->save();
|
|
|
|
return $tenant->delete();
|
|
}
|
|
|
|
/**
|
|
* 테넌트 복원
|
|
*/
|
|
public function restoreTenant(int $id): bool
|
|
{
|
|
$tenant = Tenant::onlyTrashed()->findOrFail($id);
|
|
|
|
// 삭제 정보 초기화
|
|
$tenant->deleted_by = null;
|
|
|
|
return $tenant->restore();
|
|
}
|
|
|
|
/**
|
|
* 테넌트 영구 삭제 (슈퍼관리자 전용)
|
|
*
|
|
* 1. 테넌트와 관련 데이터를 아카이브에 저장
|
|
* 2. 관련 데이터 삭제
|
|
* 3. 테넌트 영구 삭제
|
|
*/
|
|
public function forceDeleteTenant(int $id): bool
|
|
{
|
|
$tenant = Tenant::withTrashed()->findOrFail($id);
|
|
|
|
return DB::transaction(function () use ($tenant) {
|
|
// 1. 아카이브에 저장 (복원 가능하도록)
|
|
$this->archiveService->archiveTenantWithRelations($tenant);
|
|
|
|
// 2. FK 체크 비활성화 후 관련 데이터 삭제
|
|
DB::statement('SET FOREIGN_KEY_CHECKS=0');
|
|
|
|
try {
|
|
$tenant->users()->detach(); // user_tenants 관계 삭제
|
|
|
|
// 역할 하위 참조 데이터 정리
|
|
$roleIds = $tenant->roles()->pluck('id');
|
|
if ($roleIds->isNotEmpty()) {
|
|
DB::table('user_roles')->whereIn('role_id', $roleIds)->delete();
|
|
DB::table('role_menu_permissions')->whereIn('role_id', $roleIds)->delete();
|
|
DB::table('role_has_permissions')->whereIn('role_id', $roleIds)->delete();
|
|
}
|
|
|
|
$tenant->departments()->forceDelete();
|
|
$tenant->menus()->forceDelete();
|
|
$tenant->roles()->forceDelete();
|
|
|
|
// 3. 테넌트 영구 삭제
|
|
$result = $tenant->forceDelete();
|
|
} finally {
|
|
DB::statement('SET FOREIGN_KEY_CHECKS=1');
|
|
}
|
|
|
|
return $result;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 활성 테넌트 목록 (드롭다운용)
|
|
*/
|
|
public function getActiveTenants(): Collection
|
|
{
|
|
return Tenant::query()
|
|
->active()
|
|
->orderBy('company_name')
|
|
->get(['id', 'company_name', 'code']);
|
|
}
|
|
|
|
/**
|
|
* 테넌트 코드 중복 체크
|
|
*/
|
|
public function isCodeExists(string $code, ?int $excludeId = null): bool
|
|
{
|
|
$query = Tenant::where('code', $code);
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
return $query->exists();
|
|
}
|
|
|
|
/**
|
|
* 테넌트 통계
|
|
*/
|
|
public function getTenantStats(): array
|
|
{
|
|
return [
|
|
'total' => Tenant::count(),
|
|
'active' => Tenant::where('tenant_st_code', 'active')->count(),
|
|
'trial' => Tenant::where('tenant_st_code', 'trial')->count(),
|
|
'suspended' => Tenant::where('tenant_st_code', 'suspended')->count(),
|
|
'expired' => Tenant::where('tenant_st_code', 'expired')->count(),
|
|
'trashed' => Tenant::onlyTrashed()->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 모달용 테넌트 상세 정보 조회
|
|
*/
|
|
public function getTenantForModal(int $id): ?Tenant
|
|
{
|
|
return Tenant::query()
|
|
->with('deletedByUser')
|
|
->withCount(['users', 'departments', 'menus', 'roles'])
|
|
->withTrashed()
|
|
->find($id);
|
|
}
|
|
|
|
/**
|
|
* 테넌트 소속 사용자 목록 조회
|
|
*/
|
|
public function getTenantUsers(int $tenantId): Collection
|
|
{
|
|
$tenant = Tenant::find($tenantId);
|
|
if (! $tenant) {
|
|
return collect();
|
|
}
|
|
|
|
// 사용자와 함께 역할/부서 정보를 가져옴
|
|
return $tenant->users()
|
|
->with(['userRoles' => function ($query) use ($tenantId) {
|
|
$query->where('tenant_id', $tenantId)->with('role');
|
|
}, 'departmentUsers' => function ($query) use ($tenantId) {
|
|
$query->where('tenant_id', $tenantId)->with('department');
|
|
}])
|
|
->orderBy('name')
|
|
->get()
|
|
->map(function ($user) {
|
|
// 역할과 부서를 플랫하게 매핑
|
|
$user->roles = $user->userRoles->pluck('role')->filter();
|
|
$user->department = $user->departmentUsers->first()?->department;
|
|
|
|
return $user;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 테넌트 부서 목록 조회 (계층 구조)
|
|
*/
|
|
public function getTenantDepartments(int $tenantId): Collection
|
|
{
|
|
// Department 모델의 users() 관계가 잘못된 네임스페이스를 참조하므로 직접 카운트
|
|
$departments = \App\Models\Department::query()
|
|
->where('tenant_id', $tenantId)
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
// 부서별 사용자 수 직접 카운트
|
|
$userCounts = DB::table('department_user')
|
|
->whereIn('department_id', $departments->pluck('id'))
|
|
->selectRaw('department_id, count(*) as cnt')
|
|
->groupBy('department_id')
|
|
->pluck('cnt', 'department_id');
|
|
|
|
return $departments->map(function ($dept) use ($userCounts, $departments) {
|
|
// 사용자 수 설정
|
|
$dept->users_count = $userCounts[$dept->id] ?? 0;
|
|
|
|
// 계층 깊이 계산
|
|
$depth = 0;
|
|
$parentId = $dept->parent_id;
|
|
while ($parentId) {
|
|
$depth++;
|
|
$parent = $departments->firstWhere('id', $parentId);
|
|
$parentId = $parent?->parent_id;
|
|
}
|
|
$dept->depth = $depth;
|
|
|
|
// 상위 부서 이름
|
|
$dept->parent = $departments->firstWhere('id', $dept->parent_id);
|
|
|
|
return $dept;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 테넌트 역할 목록 조회
|
|
*/
|
|
public function getTenantRoles(int $tenantId): Collection
|
|
{
|
|
// Role 모델의 userRoles() 관계가 잘못된 네임스페이스를 참조하므로 직접 카운트
|
|
$roles = \App\Models\Role::query()
|
|
->where('tenant_id', $tenantId)
|
|
->withCount('permissions')
|
|
->with('permissions')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
// 역할별 사용자 수 직접 카운트
|
|
$userCounts = DB::table('user_roles')
|
|
->whereIn('role_id', $roles->pluck('id'))
|
|
->selectRaw('role_id, count(*) as cnt')
|
|
->groupBy('role_id')
|
|
->pluck('cnt', 'role_id');
|
|
|
|
return $roles->map(function ($role) use ($userCounts) {
|
|
$role->users_count = $userCounts[$role->id] ?? 0;
|
|
|
|
return $role;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 테넌트 메뉴 목록 조회 (계층 구조 - 트리 정렬)
|
|
*/
|
|
public function getTenantMenus(int $tenantId): \Illuminate\Support\Collection
|
|
{
|
|
$menus = \App\Models\Commons\Menu::query()
|
|
->where('tenant_id', $tenantId)
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
// 트리 구조로 정렬 후 플랫한 배열로 변환
|
|
return $this->flattenMenuTree($menus);
|
|
}
|
|
|
|
/**
|
|
* 트리 구조를 플랫한 배열로 변환 (depth 정보 포함)
|
|
*/
|
|
private function flattenMenuTree(Collection $menus, ?int $parentId = null, int $depth = 0): \Illuminate\Support\Collection
|
|
{
|
|
$result = collect();
|
|
|
|
$filteredMenus = $menus->where('parent_id', $parentId)->sortBy('sort_order');
|
|
|
|
foreach ($filteredMenus as $menu) {
|
|
$menu->depth = $depth;
|
|
|
|
// 자식 메뉴 존재 여부 확인
|
|
$menu->has_children = $menus->where('parent_id', $menu->id)->count() > 0;
|
|
|
|
$result->push($menu);
|
|
|
|
// 자식 메뉴 재귀적으로 추가
|
|
$children = $this->flattenMenuTree($menus, $menu->id, $depth + 1);
|
|
$result = $result->merge($children);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 테넌트 구독 정보 조회
|
|
*/
|
|
public function getTenantSubscription(int $tenantId): object
|
|
{
|
|
$tenant = Tenant::find($tenantId);
|
|
|
|
// 기본 구독 정보 (실제 구현 시 별도 테이블에서 조회)
|
|
return (object) [
|
|
'plan_name' => $tenant?->subscription_plan ?? '기본 플랜',
|
|
'status' => $tenant?->subscription_status ?? 'active',
|
|
'started_at' => $tenant?->created_at?->format('Y-m-d'),
|
|
'expires_at' => $tenant?->subscription_expires_at ?? null,
|
|
'next_billing_date' => null,
|
|
'max_users' => $tenant?->max_users ?? null,
|
|
'max_storage' => $tenant?->max_storage ?? null,
|
|
'max_storage_mb' => null,
|
|
'has_api_access' => true,
|
|
'has_advanced_reports' => false,
|
|
'features' => [
|
|
['name' => '기본 기능', 'enabled' => true],
|
|
['name' => '사용자 관리', 'enabled' => true],
|
|
['name' => '부서 관리', 'enabled' => true],
|
|
['name' => '역할 관리', 'enabled' => true],
|
|
['name' => 'API 접근', 'enabled' => true],
|
|
['name' => '고급 보고서', 'enabled' => false],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 테넌트 사용량 정보 조회
|
|
*/
|
|
public function getTenantUsage(int $tenantId): object
|
|
{
|
|
$tenant = Tenant::withCount('users')->find($tenantId);
|
|
|
|
return (object) [
|
|
'users_count' => $tenant?->users_count ?? 0,
|
|
'storage_used' => '0 MB',
|
|
'storage_used_mb' => 0,
|
|
];
|
|
}
|
|
}
|