with(['user', 'department']) ->forTenant($tenantId); // 검색 필터 (이름, 이메일, 연락처) if (! empty($filters['q'])) { $search = $filters['q']; $query->where(function ($q) use ($search) { $q->where('display_name', 'like', "%{$search}%") ->orWhereHas('user', function ($uq) use ($search) { $uq->where('name', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%") ->orWhere('phone', 'like', "%{$search}%"); }); }); } // 상태 필터 if (! empty($filters['status'])) { $query->where('employee_status', $filters['status']); } // 부서 필터 if (! empty($filters['department_id'])) { $query->where('department_id', $filters['department_id']); } // 정렬 $query->orderByRaw("FIELD(employee_status, 'active', 'leave', 'resigned')") ->orderBy('created_at', 'desc'); return $query->paginate($perPage); } /** * 사원 상세 조회 */ public function getEmployeeById(int $id): ?Employee { $tenantId = session('selected_tenant_id'); return Employee::query() ->with(['user', 'department', 'manager']) ->forTenant($tenantId) ->find($id); } /** * 사원 통계 */ public function getStats(): array { $tenantId = session('selected_tenant_id'); $baseQuery = Employee::query()->forTenant($tenantId); return [ 'total' => (clone $baseQuery)->count(), 'active' => (clone $baseQuery)->where('employee_status', 'active')->count(), 'leave' => (clone $baseQuery)->where('employee_status', 'leave')->count(), 'resigned' => (clone $baseQuery)->where('employee_status', 'resigned')->count(), ]; } /** * 테넌트 소속이지만 사원 미등록인 사용자 검색 */ public function searchTenantUsers(string $query): array { $tenantId = session('selected_tenant_id'); $builder = User::query() ->select('users.id', 'users.name', 'users.email', 'users.phone') ->join('user_tenants as ut', function ($join) use ($tenantId) { $join->on('users.id', '=', 'ut.user_id') ->where('ut.tenant_id', $tenantId) ->whereNull('ut.deleted_at'); }) ->leftJoin('tenant_user_profiles as tup', function ($join) use ($tenantId) { $join->on('users.id', '=', 'tup.user_id') ->where('tup.tenant_id', $tenantId); }) ->whereNull('tup.id') ->whereNull('users.deleted_at'); if ($query !== '') { $like = "%{$query}%"; $builder->where(function ($q) use ($like) { $q->where('users.name', 'like', $like) ->orWhere('users.email', 'like', $like) ->orWhere('users.phone', 'like', $like); }); } return $builder->orderBy('users.name')->limit(20)->get()->toArray(); } /** * 사원 등록 (User + TenantUserProfile 동시 생성) */ public function createEmployee(array $data): Employee { $tenantId = session('selected_tenant_id'); return DB::transaction(function () use ($data, $tenantId) { // 기존 사용자 선택 분기 if (! empty($data['existing_user_id'])) { $user = User::findOrFail($data['existing_user_id']); // 테넌트 소속 검증 $isMember = $user->tenants() ->wherePivot('tenant_id', $tenantId) ->wherePivotNull('deleted_at') ->exists(); if (! $isMember) { throw new \RuntimeException('해당 사용자는 현재 테넌트에 소속되어 있지 않습니다.'); } // 이미 사원 등록 여부 확인 $alreadyEmployee = Employee::where('tenant_id', $tenantId) ->where('user_id', $user->id) ->exists(); if ($alreadyEmployee) { throw new \RuntimeException('이미 사원으로 등록된 사용자입니다.'); } } else { // 신규 사용자 생성 $loginId = ! empty($data['email']) ? Str::before($data['email'], '@') : 'EMP_'.strtolower(Str::random(6)); while (User::where('user_id', $loginId)->exists()) { $loginId = $loginId.'_'.Str::random(3); } $email = ! empty($data['email']) ? $data['email'] : $loginId.'@placeholder.local'; while (User::where('email', $email)->exists()) { $email = $loginId.'_'.Str::random(3).'@placeholder.local'; } $user = User::create([ 'user_id' => $loginId, 'name' => $data['name'], 'email' => $email, 'phone' => $data['phone'] ?? null, 'password' => Hash::make($data['password'] ?? 'sam1234!'), 'role' => 'ops', 'is_active' => true, 'must_change_password' => true, 'created_by' => auth()->id(), ]); if ($tenantId) { $user->tenants()->attach($tenantId, [ 'is_active' => true, 'is_default' => true, 'joined_at' => now(), ]); } } // json_extra 구성 $jsonExtra = []; if (! empty($data['hire_date'])) { $jsonExtra['hire_date'] = $data['hire_date']; } if (! empty($data['address'])) { $jsonExtra['address'] = $data['address']; } if (! empty($data['emergency_contact'])) { $jsonExtra['emergency_contact'] = $data['emergency_contact']; } // Employee(TenantUserProfile) 생성 $employee = Employee::create([ 'tenant_id' => $tenantId, 'user_id' => $user->id, 'department_id' => $data['department_id'] ?? null, 'position_key' => $data['position_key'] ?? null, 'job_title_key' => $data['job_title_key'] ?? null, 'work_location_key' => $data['work_location_key'] ?? null, 'employment_type_key' => $data['employment_type_key'] ?? null, 'employee_status' => $data['employee_status'] ?? 'active', 'manager_user_id' => $data['manager_user_id'] ?? null, 'display_name' => $data['display_name'] ?? $data['name'], 'json_extra' => ! empty($jsonExtra) ? $jsonExtra : null, ]); return $employee->load(['user', 'department']); }); } /** * 사원 정보 수정 */ public function updateEmployee(int $id, array $data): ?Employee { $employee = $this->getEmployeeById($id); if (! $employee) { return null; } // 기본 필드 업데이트 $updateData = array_filter([ 'department_id' => $data['department_id'] ?? null, 'position_key' => $data['position_key'] ?? null, 'job_title_key' => $data['job_title_key'] ?? null, 'work_location_key' => $data['work_location_key'] ?? null, 'employment_type_key' => $data['employment_type_key'] ?? null, 'employee_status' => $data['employee_status'] ?? null, 'manager_user_id' => $data['manager_user_id'] ?? null, 'display_name' => $data['display_name'] ?? null, ], fn ($v) => $v !== null); // json_extra 업데이트 $jsonExtraKeys = ['hire_date', 'address', 'emergency_contact', 'salary', 'bank_account']; $extra = $employee->json_extra ?? []; foreach ($jsonExtraKeys as $key) { if (array_key_exists($key, $data)) { if ($data[$key] === null || $data[$key] === '') { unset($extra[$key]); } else { $extra[$key] = $data[$key]; } } } $updateData['json_extra'] = ! empty($extra) ? $extra : null; $employee->update($updateData); // User 기본정보 동기화 if ($employee->user) { $userUpdate = []; if (! empty($data['name'])) { $userUpdate['name'] = $data['name']; } if (! empty($data['email'])) { $userUpdate['email'] = $data['email']; } if (! empty($data['phone'])) { $userUpdate['phone'] = $data['phone']; } if (! empty($userUpdate)) { $employee->user->update($userUpdate); } } return $employee->fresh(['user', 'department']); } /** * 사원 삭제 (퇴직 처리) */ public function deleteEmployee(int $id): bool { $employee = $this->getEmployeeById($id); if (! $employee) { return false; } $employee->update(['employee_status' => 'resigned']); return true; } /** * 부서 목록 (드롭다운용) */ public function getDepartments(): \Illuminate\Database\Eloquent\Collection { $tenantId = session('selected_tenant_id'); return Department::query() ->where('is_active', true) ->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId)) ->orderBy('sort_order') ->orderBy('name') ->get(['id', 'name', 'code']); } /** * 직급/직책 목록 (드롭다운용) */ public function getPositions(string $type = 'rank'): \Illuminate\Database\Eloquent\Collection { return Position::query() ->forTenant() ->where('type', $type) ->where('is_active', true) ->ordered() ->get(['id', 'key', 'name']); } /** * 직급/직책 추가 */ public function createPosition(string $type, string $name): Position { $tenantId = session('selected_tenant_id'); // key 생성: 이름을 소문자+언더스코어로 변환, 한글은 그대로 $key = str_replace(' ', '_', mb_strtolower(trim($name))); // 중복 체크 후 존재하면 기존 반환 $existing = Position::query() ->forTenant() ->where('type', $type) ->where('key', $key) ->first(); if ($existing) { return $existing; } // 다음 sort_order $maxSort = Position::query() ->forTenant() ->where('type', $type) ->max('sort_order') ?? 0; return Position::create([ 'tenant_id' => $tenantId, 'type' => $type, 'key' => $key, 'name' => trim($name), 'sort_order' => $maxSort + 1, 'is_active' => true, ]); } }