'성 테넌트가 없습니다.', 'code' => 400]; } $page = isset($params['page']) ? (int)$params['page'] : 1; $size = isset($params['size']) ? (int)$params['size'] : 10; $keyword = $params['q'] ?? null; $active = $params['is_active'] ?? null; // 0/1 $sortBy = $params['sort_by'] ?? 'users.id'; $sortDir = strtolower($params['sort_dir'] ?? 'desc') === 'asc' ? 'asc' : 'desc'; $q = UserTenant::query() ->with(['user:id,name,email,phone']) ->where('tenant_id', $tenantId); if ($keyword) { $q->whereHas('user', function($sub) use ($keyword) { $sub->where(function($w) use ($keyword) { $w->where('name', 'like', "%{$keyword}%") ->orWhere('email', 'like', "%{$keyword}%") ->orWhere('phone', 'like', "%{$keyword}%"); }); }); } if ($active !== null && $active !== '') { $q->where('is_active', (int)$active); } // 조인 정렬용 $q->leftJoin('users', 'users.id', '=', 'user_tenants.user_id') ->select( 'users.id', 'users.user_id', 'users.name', 'users.email', 'users.phone', 'user_tenants.is_active', 'user_tenants.joined_at', 'user_tenants.left_at', 'user_tenants.tenant_id' ); $q->orderBy($sortBy, $sortDir); return $data = $q->paginate($size, ['*'], 'page', $page); } /** * [POST] 테넌트 사용자 추가 (기존 사용자 연결) * - 컨트롤러 store()에서 호출 * - 유저 등록 역할 부여 */ public static function store(array $params = []) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } // 신규 회원 생성 + 역할 부여 지원 $v = Validator::make($params, [ 'user_id' => 'required|string|max:255|unique:users,user_id', 'name' => 'required|string|max:255', 'email' => 'required|email|max:100|unique:users,email', 'phone' => 'nullable|string|max:30', 'password' => 'required|string|min:8|max:64', 'roles' => 'nullable|array', 'roles.*' => 'string|max:100', // 각각의 역할 이름 ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; } $payload = $v->validated(); return DB::transaction(function () use ($payload, $tenantId) { // 신규 사용자 생성 $user = User::create([ 'user_id' => $payload['user_id'], 'name' => $payload['name'], 'email' => $payload['email'], 'phone' => $payload['phone'] ?? null, 'password' => $payload['password'], // 캐스트가 알아서 해싱 ]); // 현재 테넌트에 활성 연결 UserTenant::create([ 'user_id' => $user->id, 'tenant_id' => $tenantId, 'is_active' => 1, 'is_default' => 0, 'joined_at' => now(), ]); // 역할 부여 (Spatie Permission teams 모드 가정) if (!empty($payload['roles']) && method_exists($user, 'assignRole')) { $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; app()->instance('permission.team_id', $tenantId); try { foreach ($payload['roles'] as $roleName) { $user->assignRole($roleName); } } finally { app()->instance('permission.team_id', $previousTeam); } } return [ 'user' => $user->only(['id','user_id','name','email','phone']), ]; }); } /** * [GET] 테넌트 사용자 단건 조회 * - 컨트롤러 show()에서 호출 */ public static function show(int $userNo) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } if (!$userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } $user = User::whereHas('userTenants')->find($userNo); if (!$user) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } return $user; } /** * [PUT/PATCH] 테넌트 사용자 정보 수정 * - 회원 기본정보(user_id, name, email, phone, password) 변경 * - 역할(roles) 변경 및 삭제 처리 */ public static function update(array $params = [], int $userNo) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } if (!$userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } // 1) 유저 존재/테넌트 소속 확인 $user = User::find($userNo); if (!$user) { return ['error' => '해당 회원을 찾을 수 없습니다.', 'code' => 404]; } $linked = UserTenant::where('tenant_id', $tenantId) ->where('user_id', $userNo) ->exists(); if (!$linked) { return ['error' => '이 테넌트에 소속된 회원이 아닙니다.', 'code' => 403]; } // 2) 프로필 + roles만 수정 $v = Validator::make($params, [ 'user_id' => ['nullable','string','max:255', Rule::unique('users','user_id')->ignore($userNo)], 'name' => 'nullable|string|max:255', 'email' => ['nullable','email','max:100', Rule::unique('users','email')->ignore($userNo)], 'phone' => 'nullable|string|max:30', 'password' => 'nullable|string|min:8|max:64', 'roles' => 'nullable|array', 'roles.*' => 'string|max:100', ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; } $payload = $v->validated(); // 아무 필드도 없으면 방어 $updatableKeys = ['user_id','name','email','phone','password']; $hasProfileInput = (bool) array_intersect(array_keys($payload), $updatableKeys); $hasRolesInput = array_key_exists('roles', $payload); if (!$hasProfileInput && !$hasRolesInput) { return ['error' => '수정할 항목이 없습니다.', 'code' => 422]; } return DB::transaction(function () use ($user, $payload, $tenantId, $updatableKeys) { // 3) 프로필 업데이트 (제공된 키만 반영) $updateData = []; foreach ($updatableKeys as $k) { if (array_key_exists($k, $payload)) { $updateData[$k] = $payload[$k]; } } // 비밀번호 처리 if (array_key_exists('password', $updateData)) { if ($updateData['password'] === null || $updateData['password'] === '') { unset($updateData['password']); // 빈 값 들어오면 무시 } } if (!empty($updateData)) { $user->fill($updateData); $user->save(); } // 4) 역할 수정 (teams 모드: 테넌트 컨텍스트로 sync) if (array_key_exists('roles', $payload) && method_exists($user, 'syncRoles')) { $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; app()->instance('permission.team_id', $tenantId); try { // roles 키가 있으면 그 값으로 덮어쓰기 (빈 배열이면 모두 제거) $roles = $payload['roles'] ?? []; $user->syncRoles($roles); } finally { app()->instance('permission.team_id', $previousTeam); } } return [ 'user' => $user->only(['id','user_id','name','email','phone']), 'roles' => method_exists($user, 'getRoleNames') ? $user->getRoleNames() : [], ]; }); } /** * [DELETE] 테넌트 사용자 삭제(연결 해제) * - soft delete + left_at 기록 */ public static function destroy(int $userNo) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } if (!$userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } $ut = UserTenant::where('user_id',$userNo) ->where('tenant_id', $tenantId) ->first(); if (!$ut) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } $ut->left_at = now(); $ut->save(); $ut->delete(); // SoftDeletes 가정 return 'success'; } /** * [POST] 삭제 복구 */ public static function restore(int $userNo) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } if (!$userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } $ut = UserTenant::withTrashed() ->where('tenant_id', $tenantId) ->where('user_id', $userNo) ->first(); if (!$ut) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } if ($ut->trashed()) { $ut->restore(); $ut->left_at = null; $ut->save(); } return 'success'; } /** * [PATCH] 활성/비활성 토글 */ public static function toggle(int $userNo) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } if (!$userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } $ut = UserTenant::where('tenant_id', $tenantId) ->where('user_id', $userNo) ->first(); if (!$ut) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } $ut->is_active = $ut->is_active ? 0 : 1; $ut->save(); return ['is_active' => $ut->is_active]; } /** * [POST] 역할 부여 (Spatie Permission - teams 사용 가정) * - params: user_id, role_name */ public static function attach(array $params = []) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $v = Validator::make($params, [ 'user_id' => 'required|integer|exists:users,id', 'role_name' => 'required|string|max:100', ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; } $user = User::find($params['user_id']); if (!method_exists($user, 'assignRole')) { // Spatie 미사용 환경 방어 return ['error' => '역할 시스템이 활성화되어 있지 않습니다.', 'code' => 501]; } // teams(tenant) 스코프 $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; app()->instance('permission.team_id', $tenantId); try { $user->assignRole($params['role_name']); } finally { // 원복 app()->instance('permission.team_id', $previousTeam); } return 'success'; } /** * [POST] 역할 해제 (Spatie Permission - teams 사용 가정) * - params: user_id, role_name */ public static function detach(array $params = []) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $v = Validator::make($params, [ 'user_id' => 'required|integer|exists:users,id', 'role_name' => 'required|string|max:100', ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; } $user = User::find($params['user_id']); if (!method_exists($user, 'removeRole')) { return ['error' => '역할 시스템이 활성화되어 있지 않습니다.', 'code' => 501]; } $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; app()->instance('permission.team_id', $tenantId); try { $user->removeRole($params['role_name']); } finally { app()->instance('permission.team_id', $previousTeam); } return 'success'; } /** * [POST] 테넌트 사용자 비밀번호 초기화 * - (보안) 관리자 권한 확인은 미들웨어/가드에서 처리 가정 * - 새 임시 비밀번호를 설정(응답으로 직접 노출 X 권장) * - 여기서는 옵션에 따라 노출/미노출 선택 가능하도록 구현 */ public static function reset(array $params = [],int $userNo) { $tenantId = app('tenant_id'); if (!$tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } if (!$userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } $v = Validator::make($params, [ 'new_password' => 'nullable|string|min:8|max:64', 'return_password' => 'nullable|in:0,1', // 1이면 응답에 임시 비번 포함(개발용) ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; } $payload = $v->validated(); $user = User::find($userNo); if (!$user) { return ['error' => '유저를 찾을 수 없습니다.', 'code' => 404]; } $new = $payload['new_password'] ?? Str::random(12); $user->password = $new; $user->save(); // (선택) 기존 토큰 무효화 // if (method_exists($user, 'tokens')) { $user->tokens()->delete(); } $resp = ['status' => 'ok']; if (!empty($payload['return_password'])) { // 운영에선 반환하지 말고 메일/문자 발송을 권장 $resp['temp_password'] = $new; } return $resp; } }