withTrashed(); // 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음 if (! auth()->user()?->is_super_admin) { $query->where('is_super_admin', false); } // 역할/부서/테넌트 관계 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(['tenants']); } // 테넌트 필터링 (user_tenants pivot을 통한 필터링) if ($tenantId) { $query->whereHas('tenants', function ($q) use ($tenantId) { $q->where('tenants.id', $tenantId); }); } // Soft Delete 필터 if (isset($filters['trashed'])) { if ($filters['trashed'] === 'only') { $query->onlyTrashed(); } elseif ($filters['trashed'] === 'with') { $query->withTrashed(); } } // 검색 필터 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); } /** * 사용자 생성 * - 본사(HQ): 임의 비밀번호 생성 + 메일 발송 * - 비본사: 입력된 비밀번호 사용 (메일 발송 안 함) */ public function createUser(array $data): User { $tenantId = session('selected_tenant_id'); // 비밀번호 처리: 입력된 비밀번호가 있으면 사용, 없으면 자동 생성 $passwordProvided = ! empty($data['password']); if ($passwordProvided) { // 비본사: 입력된 비밀번호 사용 $data['password'] = Hash::make($data['password']); $plainPassword = null; // 메일 발송하지 않음 } else { // 본사: 임의 비밀번호 생성 (8자리 영문+숫자) $plainPassword = $this->generateRandomPassword(); $data['password'] = Hash::make($plainPassword); } // password_confirmation은 User 모델의 fillable이 아니므로 제거 unset($data['password_confirmation']); // is_active 처리 $data['is_active'] = isset($data['is_active']) && $data['is_active'] == '1'; // 최초 로그인 시 비밀번호 변경 필요 $data['must_change_password'] = true; // 생성자 정보 $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(), ]); // 역할/부서 동기화 $roleIds = $data['role_ids'] ?? []; $departmentIds = $data['department_ids'] ?? []; $this->syncRoles($user, $tenantId, $roleIds); $this->syncDepartments($user, $tenantId, $departmentIds); } // 본사만 비밀번호 안내 메일 발송 (비본사는 관리자가 직접 알려줌) if ($plainPassword !== null) { $this->sendPasswordMail($user, $plainPassword, true); } return $user; } /** * 비밀번호 초기화 (관리자용: 임의 비밀번호 생성 + 메일 발송) */ public function resetPassword(int $id): bool { $user = $this->getUserById($id); if (! $user) { return false; } // 임의 비밀번호 생성 $plainPassword = $this->generateRandomPassword(); // 비밀번호 업데이트 + 비밀번호 변경 필요 플래그 $user->password = Hash::make($plainPassword); $user->must_change_password = true; $user->updated_by = auth()->id(); $user->save(); // 비밀번호 초기화 안내 메일 발송 $this->sendPasswordMail($user, $plainPassword, false); return true; } /** * 임의 비밀번호 생성 (8자리 영문+숫자 조합) */ private function generateRandomPassword(int $length = 8): string { // 영문 대소문자 + 숫자 조합으로 가독성 좋은 비밀번호 생성 // 혼동되는 문자 제외: 0, O, l, 1, I $chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789'; return substr(str_shuffle(str_repeat($chars, 3)), 0, $length); } /** * 비밀번호 안내 메일 발송 */ private function sendPasswordMail(User $user, string $password, bool $isNewUser): void { Mail::to($user->email)->send(new UserPasswordMail($user, $password, $isNewUser)); } /** * 사용자 수정 */ public function updateUser(int $id, array $data): bool { $user = $this->getUserById($id); if (! $user) { return false; } $tenantId = session('selected_tenant_id'); // 비밀번호가 입력된 경우만 업데이트 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'; // is_super_admin 처리 (슈퍼관리자만 수정 가능하므로 validated 데이터에 있을 때만) if (array_key_exists('is_super_admin', $data)) { $data['is_super_admin'] = $data['is_super_admin'] == '1'; } // 수정자 정보 $data['updated_by'] = auth()->id(); // 역할/부서 동기화 (테넌트가 선택된 경우) 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']); return $user->update($data); } /** * 사용자 역할 동기화 (특정 테넌트) */ 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(), ]); } } /** * 사용자 삭제 (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(); } /** * 사용자 복원 */ public function restoreUser(int $id): bool { $user = User::onlyTrashed()->findOrFail($id); return $user->restore(); } /** * 사용자 영구 삭제 (슈퍼관리자 전용) * * 1. 사용자 데이터를 아카이브에 저장 * 2. 관련 데이터 삭제 * 3. 사용자 영구 삭제 */ public function forceDeleteUser(int $id): bool { $user = User::withTrashed()->findOrFail($id); return DB::transaction(function () use ($user) { // 1. 아카이브에 저장 (복원 가능하도록) $this->archiveService->archiveUser($user); // 2. 관련 데이터 삭제 $user->tenants()->detach(); // user_tenants 관계 삭제 // 3. 사용자 영구 삭제 return $user->forceDelete(); }); } /** * 모달용 사용자 상세 정보 조회 */ 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; } /** * 활성 사용자 목록 조회 (드롭다운용) */ public function getActiveUsers() { $tenantId = session('selected_tenant_id'); $query = User::query()->where('is_active', true); // 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음 if (! auth()->user()?->is_super_admin) { $query->where('is_super_admin', false); } // 테넌트 필터링 (user_tenants pivot을 통한 필터링) if ($tenantId) { $query->whereHas('tenants', function ($q) use ($tenantId) { $q->where('tenants.id', $tenantId); }); } return $query->orderBy('name')->get(); } /** * 슈퍼관리자 보호: 일반관리자가 슈퍼관리자에 접근할 수 있는지 확인 * * @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; } }