2025-11-24 18:48:07 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
2025-11-26 20:28:07 +09:00
|
|
|
use App\Models\DepartmentUser;
|
2025-11-24 18:48:07 +09:00
|
|
|
use App\Models\User;
|
2025-11-26 20:28:07 +09:00
|
|
|
use App\Models\UserRole;
|
2025-11-24 18:48:07 +09:00
|
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
2025-12-01 00:13:12 +09:00
|
|
|
use Illuminate\Support\Facades\DB;
|
2025-11-24 18:48:07 +09:00
|
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
|
|
|
|
|
|
class UserService
|
|
|
|
|
{
|
2025-12-01 00:13:12 +09:00
|
|
|
public function __construct(
|
|
|
|
|
private readonly ArchiveService $archiveService
|
|
|
|
|
) {}
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
/**
|
|
|
|
|
* 사용자 목록 조회 (페이지네이션)
|
|
|
|
|
*/
|
|
|
|
|
public function getUsers(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
2025-11-24 19:30:36 +09:00
|
|
|
$query = User::query()->withTrashed();
|
2025-11-24 18:48:07 +09:00
|
|
|
|
2025-12-01 00:13:12 +09:00
|
|
|
// 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음
|
|
|
|
|
if (! auth()->user()?->is_super_admin) {
|
|
|
|
|
$query->where('is_super_admin', false);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 20:05:27 +09:00
|
|
|
// 역할/부서 관계 eager loading (테넌트별)
|
|
|
|
|
if ($tenantId) {
|
|
|
|
|
$query->with([
|
|
|
|
|
'userRoles' => fn ($q) => $q->where('tenant_id', $tenantId)->with('role'),
|
|
|
|
|
'departmentUsers' => fn ($q) => $q->where('tenant_id', $tenantId)->with('department'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
// 테넌트 필터링 (user_tenants pivot을 통한 필터링)
|
|
|
|
|
if ($tenantId) {
|
|
|
|
|
$query->whereHas('tenants', function ($q) use ($tenantId) {
|
|
|
|
|
$q->where('tenants.id', $tenantId);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:30:36 +09:00
|
|
|
// Soft Delete 필터
|
|
|
|
|
if (isset($filters['trashed'])) {
|
|
|
|
|
if ($filters['trashed'] === 'only') {
|
|
|
|
|
$query->onlyTrashed();
|
|
|
|
|
} elseif ($filters['trashed'] === 'with') {
|
|
|
|
|
$query->withTrashed();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
// 검색 필터
|
|
|
|
|
if (! empty($filters['search'])) {
|
|
|
|
|
$search = $filters['search'];
|
|
|
|
|
$query->where(function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('email', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('phone', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('user_id', 'like', "%{$search}%");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 활성 상태 필터
|
|
|
|
|
if (isset($filters['is_active'])) {
|
|
|
|
|
$query->where('is_active', $filters['is_active']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $query->orderBy('created_at', 'desc')->paginate($perPage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 상세 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getUserById(int $id): ?User
|
|
|
|
|
{
|
|
|
|
|
return User::find($id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 생성
|
|
|
|
|
*/
|
|
|
|
|
public function createUser(array $data): User
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
// 비밀번호 해싱
|
|
|
|
|
if (isset($data['password'])) {
|
|
|
|
|
$data['password'] = Hash::make($data['password']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// is_active 처리
|
|
|
|
|
$data['is_active'] = isset($data['is_active']) && $data['is_active'] == '1';
|
|
|
|
|
|
|
|
|
|
// 생성자 정보
|
|
|
|
|
$data['created_by'] = auth()->id();
|
|
|
|
|
|
|
|
|
|
// 사용자 생성
|
|
|
|
|
$user = User::create($data);
|
|
|
|
|
|
|
|
|
|
// user_tenants pivot에 관계 추가
|
|
|
|
|
if ($tenantId) {
|
|
|
|
|
$user->tenants()->attach($tenantId, [
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
'is_default' => true,
|
|
|
|
|
'joined_at' => now(),
|
|
|
|
|
]);
|
2025-11-26 20:28:07 +09:00
|
|
|
|
|
|
|
|
// 역할/부서 동기화
|
|
|
|
|
$roleIds = $data['role_ids'] ?? [];
|
|
|
|
|
$departmentIds = $data['department_ids'] ?? [];
|
|
|
|
|
|
|
|
|
|
$this->syncRoles($user, $tenantId, $roleIds);
|
|
|
|
|
$this->syncDepartments($user, $tenantId, $departmentIds);
|
2025-11-24 18:48:07 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $user;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 수정
|
|
|
|
|
*/
|
|
|
|
|
public function updateUser(int $id, array $data): bool
|
|
|
|
|
{
|
|
|
|
|
$user = $this->getUserById($id);
|
|
|
|
|
if (! $user) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 20:28:07 +09:00
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
// 비밀번호가 입력된 경우만 업데이트
|
|
|
|
|
if (! empty($data['password'])) {
|
|
|
|
|
$data['password'] = Hash::make($data['password']);
|
|
|
|
|
} else {
|
|
|
|
|
unset($data['password']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// is_active 처리
|
|
|
|
|
$data['is_active'] = isset($data['is_active']) && $data['is_active'] == '1';
|
|
|
|
|
|
|
|
|
|
// 수정자 정보
|
|
|
|
|
$data['updated_by'] = auth()->id();
|
|
|
|
|
|
2025-11-26 20:28:07 +09:00
|
|
|
// 역할/부서 동기화 (테넌트가 선택된 경우)
|
|
|
|
|
if ($tenantId) {
|
|
|
|
|
$roleIds = $data['role_ids'] ?? [];
|
|
|
|
|
$departmentIds = $data['department_ids'] ?? [];
|
|
|
|
|
|
|
|
|
|
$this->syncRoles($user, $tenantId, $roleIds);
|
|
|
|
|
$this->syncDepartments($user, $tenantId, $departmentIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// role_ids, department_ids는 User 모델의 fillable이 아니므로 제거
|
|
|
|
|
unset($data['role_ids'], $data['department_ids']);
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
return $user->update($data);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 20:28:07 +09:00
|
|
|
/**
|
|
|
|
|
* 사용자 역할 동기화 (특정 테넌트)
|
|
|
|
|
*/
|
|
|
|
|
public function syncRoles(User $user, int $tenantId, array $roleIds): void
|
|
|
|
|
{
|
|
|
|
|
// 기존 역할 삭제 (해당 테넌트만) - forceDelete로 실제 삭제
|
|
|
|
|
UserRole::withTrashed()
|
|
|
|
|
->where('user_id', $user->id)
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->forceDelete();
|
|
|
|
|
|
|
|
|
|
// 새 역할 추가
|
|
|
|
|
foreach ($roleIds as $roleId) {
|
|
|
|
|
UserRole::create([
|
|
|
|
|
'user_id' => $user->id,
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'role_id' => $roleId,
|
|
|
|
|
'assigned_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 부서 동기화 (특정 테넌트)
|
|
|
|
|
*/
|
|
|
|
|
public function syncDepartments(User $user, int $tenantId, array $departmentIds): void
|
|
|
|
|
{
|
|
|
|
|
// 기존 부서 삭제 (해당 테넌트만) - forceDelete로 실제 삭제
|
|
|
|
|
DepartmentUser::withTrashed()
|
|
|
|
|
->where('user_id', $user->id)
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->forceDelete();
|
|
|
|
|
|
|
|
|
|
// 새 부서 추가 (첫 번째를 primary로)
|
|
|
|
|
foreach ($departmentIds as $index => $departmentId) {
|
|
|
|
|
DepartmentUser::create([
|
|
|
|
|
'user_id' => $user->id,
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'department_id' => $departmentId,
|
|
|
|
|
'is_primary' => $index === 0,
|
|
|
|
|
'joined_at' => now(),
|
|
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
/**
|
|
|
|
|
* 사용자 삭제 (Soft Delete)
|
|
|
|
|
*/
|
|
|
|
|
public function deleteUser(int $id): bool
|
|
|
|
|
{
|
|
|
|
|
$user = $this->getUserById($id);
|
|
|
|
|
if (! $user) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$user->deleted_by = auth()->id();
|
|
|
|
|
$user->save();
|
|
|
|
|
|
|
|
|
|
return $user->delete();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:30:36 +09:00
|
|
|
/**
|
|
|
|
|
* 사용자 복원
|
|
|
|
|
*/
|
|
|
|
|
public function restoreUser(int $id): bool
|
|
|
|
|
{
|
|
|
|
|
$user = User::onlyTrashed()->findOrFail($id);
|
2025-11-25 11:06:03 +09:00
|
|
|
|
2025-11-24 19:30:36 +09:00
|
|
|
return $user->restore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 영구 삭제 (슈퍼관리자 전용)
|
2025-12-01 00:13:12 +09:00
|
|
|
*
|
|
|
|
|
* 1. 사용자 데이터를 아카이브에 저장
|
|
|
|
|
* 2. 관련 데이터 삭제
|
|
|
|
|
* 3. 사용자 영구 삭제
|
2025-11-24 19:30:36 +09:00
|
|
|
*/
|
|
|
|
|
public function forceDeleteUser(int $id): bool
|
|
|
|
|
{
|
|
|
|
|
$user = User::withTrashed()->findOrFail($id);
|
|
|
|
|
|
2025-12-01 00:13:12 +09:00
|
|
|
return DB::transaction(function () use ($user) {
|
|
|
|
|
// 1. 아카이브에 저장 (복원 가능하도록)
|
|
|
|
|
$this->archiveService->archiveUser($user);
|
|
|
|
|
|
|
|
|
|
// 2. 관련 데이터 삭제
|
|
|
|
|
$user->tenants()->detach(); // user_tenants 관계 삭제
|
2025-11-24 19:30:36 +09:00
|
|
|
|
2025-12-01 00:13:12 +09:00
|
|
|
// 3. 사용자 영구 삭제
|
|
|
|
|
return $user->forceDelete();
|
|
|
|
|
});
|
2025-11-24 19:30:36 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-27 20:05:27 +09:00
|
|
|
/**
|
|
|
|
|
* 모달용 사용자 상세 정보 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getUserForModal(int $id): ?User
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
$query = User::query()
|
|
|
|
|
->with('deletedByUser')
|
|
|
|
|
->withTrashed();
|
|
|
|
|
|
|
|
|
|
// 역할/부서 관계 eager loading (테넌트별)
|
|
|
|
|
if ($tenantId) {
|
|
|
|
|
$query->with([
|
|
|
|
|
'userRoles' => fn ($q) => $q->where('tenant_id', $tenantId)->with('role'),
|
|
|
|
|
'departmentUsers' => fn ($q) => $q->where('tenant_id', $tenantId)->with('department'),
|
|
|
|
|
'tenants',
|
|
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
$query->with(['userRoles.role', 'departmentUsers.department', 'tenants']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$user = $query->find($id);
|
|
|
|
|
|
|
|
|
|
// 권한 카운트 추가
|
|
|
|
|
if ($user && $tenantId) {
|
|
|
|
|
$permissionCounts = $this->getUserPermissionCounts($user->id, $tenantId);
|
|
|
|
|
$user->web_permission_count = $permissionCounts['web'];
|
|
|
|
|
$user->api_permission_count = $permissionCounts['api'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $user;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자별 guard별 권한 개수 조회 (역할 + 부서 + 개인 오버라이드 통합)
|
|
|
|
|
*/
|
|
|
|
|
private function getUserPermissionCounts(int $userId, int $tenantId): array
|
|
|
|
|
{
|
|
|
|
|
$result = ['web' => 0, 'api' => 0];
|
|
|
|
|
$now = now();
|
|
|
|
|
|
|
|
|
|
foreach (['web', 'api'] as $guardName) {
|
|
|
|
|
// 1. 역할 권한
|
|
|
|
|
$rolePermissions = \DB::table('model_has_roles as mhr')
|
|
|
|
|
->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id')
|
|
|
|
|
->join('permissions as p', 'p.id', '=', 'rhp.permission_id')
|
|
|
|
|
->where('mhr.model_type', User::class)
|
|
|
|
|
->where('mhr.model_id', $userId)
|
|
|
|
|
->where('p.guard_name', $guardName)
|
|
|
|
|
->where('p.name', 'like', 'menu:%')
|
|
|
|
|
->pluck('p.name')
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
// 2. 부서 권한
|
|
|
|
|
$deptPermissions = \DB::table('department_user as du')
|
|
|
|
|
->join('permission_overrides as po', function ($j) use ($now, $tenantId) {
|
|
|
|
|
$j->on('po.model_id', '=', 'du.department_id')
|
|
|
|
|
->where('po.model_type', 'App\\Models\\Tenants\\Department')
|
|
|
|
|
->where('po.tenant_id', $tenantId)
|
|
|
|
|
->whereNull('po.deleted_at')
|
|
|
|
|
->where('po.effect', 1)
|
|
|
|
|
->where(function ($w) use ($now) {
|
|
|
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
|
|
|
})
|
|
|
|
|
->where(function ($w) use ($now) {
|
|
|
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
|
|
|
->whereNull('du.deleted_at')
|
|
|
|
|
->where('du.user_id', $userId)
|
|
|
|
|
->where('du.tenant_id', $tenantId)
|
|
|
|
|
->where('p.guard_name', $guardName)
|
|
|
|
|
->where('p.name', 'like', 'menu:%')
|
|
|
|
|
->pluck('p.name')
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
// 3. 개인 오버라이드 (ALLOW)
|
|
|
|
|
$personalAllows = \DB::table('permission_overrides as po')
|
|
|
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
|
|
|
->where('po.model_type', User::class)
|
|
|
|
|
->where('po.model_id', $userId)
|
|
|
|
|
->where('po.tenant_id', $tenantId)
|
|
|
|
|
->where('po.effect', 1)
|
|
|
|
|
->whereNull('po.deleted_at')
|
|
|
|
|
->where(function ($w) use ($now) {
|
|
|
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
|
|
|
})
|
|
|
|
|
->where(function ($w) use ($now) {
|
|
|
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
|
|
|
})
|
|
|
|
|
->where('p.guard_name', $guardName)
|
|
|
|
|
->where('p.name', 'like', 'menu:%')
|
|
|
|
|
->pluck('p.name')
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
// 4. 개인 오버라이드 (DENY) - 제외할 권한
|
|
|
|
|
$personalDenies = \DB::table('permission_overrides as po')
|
|
|
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
|
|
|
->where('po.model_type', User::class)
|
|
|
|
|
->where('po.model_id', $userId)
|
|
|
|
|
->where('po.tenant_id', $tenantId)
|
|
|
|
|
->where('po.effect', 0)
|
|
|
|
|
->whereNull('po.deleted_at')
|
|
|
|
|
->where(function ($w) use ($now) {
|
|
|
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
|
|
|
})
|
|
|
|
|
->where(function ($w) use ($now) {
|
|
|
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
|
|
|
})
|
|
|
|
|
->where('p.guard_name', $guardName)
|
|
|
|
|
->where('p.name', 'like', 'menu:%')
|
|
|
|
|
->pluck('p.name')
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
// 통합: (역할 OR 부서 OR 개인ALLOW) - 개인DENY
|
|
|
|
|
$allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows));
|
|
|
|
|
$effectivePermissions = array_diff($allAllowed, $personalDenies);
|
|
|
|
|
|
|
|
|
|
$result[$guardName] = count($effectivePermissions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
/**
|
|
|
|
|
* 활성 사용자 목록 조회 (드롭다운용)
|
|
|
|
|
*/
|
|
|
|
|
public function getActiveUsers()
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
$query = User::query()->where('is_active', true);
|
|
|
|
|
|
2025-12-01 00:13:12 +09:00
|
|
|
// 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음
|
|
|
|
|
if (! auth()->user()?->is_super_admin) {
|
|
|
|
|
$query->where('is_super_admin', false);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 18:48:07 +09:00
|
|
|
// 테넌트 필터링 (user_tenants pivot을 통한 필터링)
|
|
|
|
|
if ($tenantId) {
|
|
|
|
|
$query->whereHas('tenants', function ($q) use ($tenantId) {
|
|
|
|
|
$q->where('tenants.id', $tenantId);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $query->orderBy('name')->get();
|
|
|
|
|
}
|
2025-12-01 00:13:12 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 슈퍼관리자 보호: 일반관리자가 슈퍼관리자에 접근할 수 있는지 확인
|
|
|
|
|
*
|
|
|
|
|
* @param int $targetUserId 대상 사용자 ID
|
|
|
|
|
* @return bool true면 접근 가능, false면 접근 불가
|
|
|
|
|
*/
|
|
|
|
|
public function canAccessUser(int $targetUserId): bool
|
|
|
|
|
{
|
|
|
|
|
// withTrashed()를 사용하여 soft-deleted 사용자도 확인 (복원 시 필요)
|
|
|
|
|
$targetUser = User::withTrashed()->find($targetUserId);
|
|
|
|
|
$currentUser = auth()->user();
|
|
|
|
|
|
|
|
|
|
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 접근 불가
|
|
|
|
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-11-24 18:48:07 +09:00
|
|
|
}
|