Files
sam-manage/app/Services/TenantService.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,
];
}
}