- 견적확정 시 업체명/현장명/담당자/연락처 필수 검증 추가 (QuoteService) - 작업지시 stats API에 by_process 공정별 카운트 반환 추가 - 작업지시 목록/상세 쿼리에 수주 개소(rootNodes) 연관 로딩 - 작업지시 품목에 sourceOrderItem.node 관계 추가 - 입고관리 완료건 수정 허용 및 재고 차이 조정 - work_order_step_progress 테이블 마이그레이션 - receivings 테이블 options 컬럼 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
376 lines
14 KiB
PHP
376 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Commons\Menu;
|
|
use App\Models\Members\User;
|
|
use App\Models\Members\UserTenant;
|
|
use App\Models\Tenants\Department;
|
|
use App\Models\Tenants\Tenant;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
class MemberService
|
|
{
|
|
/**
|
|
* 회원 조회(리스트)
|
|
*/
|
|
public static function getMembers($request)
|
|
{
|
|
|
|
$pageNo = $request->page ?? 1;
|
|
$pageSize = $request->size ?? 10;
|
|
|
|
$query = User::whereHas('userTenants', function ($q) {
|
|
$q->active();
|
|
})->debug();
|
|
$query = $query->paginate($pageSize, ['*'], 'page', $pageNo);
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
/**
|
|
* 단일 회원 조회
|
|
*/
|
|
public static function getMember(int $userNo)
|
|
{
|
|
$query = User::whereHas('userTenants', function ($q) {
|
|
$q->active();
|
|
})->where('id', $userNo);
|
|
|
|
return $query->first();
|
|
}
|
|
|
|
/**
|
|
* 내정보 확인
|
|
*/
|
|
public static function getMyInfo()
|
|
{
|
|
$apiUser = app('api_user');
|
|
$user = User::find($apiUser);
|
|
$data['user'] = $user;
|
|
|
|
$tenantId = app('tenant_id');
|
|
if ($tenantId) {
|
|
$tenant = Tenant::find($tenantId);
|
|
$data['tenant'] = $tenant;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 내정보 수정
|
|
*/
|
|
public static function getMyUpdate($request)
|
|
{
|
|
|
|
$apiUser = app('api_user');
|
|
|
|
// 요청으로 받은 수정 데이터 유효성 검사
|
|
$validatedData = $request->validate([
|
|
'name' => 'sometimes|string|max:255',
|
|
'phone' => 'sometimes|string|max:20',
|
|
'email' => 'sometimes|email|max:100',
|
|
'options' => 'nullable|json',
|
|
'profile_photo_path' => 'nullable|string|max:255',
|
|
]);
|
|
|
|
$user = User::find($apiUser);
|
|
|
|
if (! $user) {
|
|
return ['error' => 'User not found.', 'code' => 404];
|
|
}
|
|
|
|
// 사용자 정보 업데이트
|
|
$user->update($validatedData);
|
|
|
|
// 수정 성공 시 success 반환
|
|
return 'success';
|
|
}
|
|
|
|
/**
|
|
* 내 비밀번호 수정
|
|
*/
|
|
public static function setMyPassword($request)
|
|
{
|
|
$apiUserId = app('api_user'); // 현재 로그인한 사용자 PK
|
|
|
|
// 유효성 검사 (확인 비밀번호는 선택)
|
|
$validated = $request->validate([
|
|
'current_password' => 'required|string',
|
|
'new_password' => 'required|string|min:8|max:64',
|
|
]);
|
|
|
|
// 선택적으로 확인 비밀번호가 온 경우 체크
|
|
if ($request->filled('new_password_confirmation') &&
|
|
$request->input('new_password_confirmation') !== $validated['new_password']) {
|
|
return ['error' => '비밀번호 확인이 일치하지 않습니다.', 'code' => 400];
|
|
}
|
|
|
|
// 유저 조회
|
|
$user = User::find($apiUserId);
|
|
if (! $user) {
|
|
return ['error' => '유저를 찾을 수 없음', 'code' => 404];
|
|
}
|
|
|
|
// 현재 비밀번호 확인
|
|
if (! Hash::check($validated['current_password'], $user->password)) {
|
|
return ['error' => '현재 비밀번호가 일치하지 않습니다.', 'code' => 400];
|
|
}
|
|
|
|
// 기존 비밀번호와 동일한지 방지
|
|
if (Hash::check($validated['new_password'], $user->password)) {
|
|
return ['error' => '새 비밀번호가 기존 비밀번호와 동일합니다.', 'code' => 400];
|
|
}
|
|
|
|
// 비밀번호 변경 (guarded 우회: 직접 대입 + save)
|
|
$user->password = Hash::make($validated['new_password']);
|
|
$saved = $user->save();
|
|
|
|
// (선택) 모든 기존 토큰 무효화하려면 아래 주석 해제
|
|
// $user->tokens()->delete();
|
|
|
|
return 'success';
|
|
}
|
|
|
|
/**
|
|
* 나의 테넌트 목록
|
|
*/
|
|
public static function getMyTenants()
|
|
{
|
|
|
|
$apiUser = app('api_user');
|
|
$data = UserTenant::join('tenants', 'user_tenants.tenant_id', '=', 'tenants.id')
|
|
->where('user_tenants.user_id', $apiUser)
|
|
->get([
|
|
'tenants.id',
|
|
'tenants.company_name',
|
|
'user_tenants.is_active',
|
|
'user_tenants.is_default',
|
|
]);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 나의 테넌트 전환
|
|
*/
|
|
public static function switchMyTenant(int $tenantId)
|
|
{
|
|
|
|
$apiUser = app('api_user');
|
|
|
|
// 1) 현재 유저의 기본 테넌트를 모두 해제
|
|
UserTenant::withoutGlobalScopes()
|
|
->where('user_id', $apiUser)
|
|
->where('is_default', 1)
|
|
->update(['is_default' => 0]);
|
|
|
|
// 2) 지정한 tenant_id를 기본 테넌트로 설정
|
|
$updated = UserTenant::withoutGlobalScopes()
|
|
->where('user_id', $apiUser)
|
|
->where('tenant_id', $tenantId)
|
|
->update(['is_default' => 1]);
|
|
|
|
if (! $updated) {
|
|
return ['error' => '해당 테넌트를 찾을 수 없습니다.', 'code' => 404];
|
|
}
|
|
|
|
return 'success';
|
|
}
|
|
|
|
/**
|
|
* 로그인 사용자 정보 조회 (테넌트 + 메뉴 권한 포함)
|
|
*/
|
|
public static function getUserInfoForLogin(int $userId): array
|
|
{
|
|
// 1. 사용자 기본 정보 조회
|
|
$user = User::find($userId);
|
|
if (! $user) {
|
|
throw new \Exception('사용자를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 기본 사용자 정보 (민감 정보 제외)
|
|
$userInfo = [
|
|
'id' => $user->id,
|
|
'user_id' => $user->user_id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'phone' => $user->phone,
|
|
'department' => null,
|
|
];
|
|
|
|
// 2. 활성 테넌트 조회 (1순위: is_default=1, 2순위: is_active=1 첫 번째)
|
|
$userTenants = UserTenant::with('tenant')
|
|
->where('user_id', $userId)
|
|
->where('is_active', 1)
|
|
->orderByDesc('is_default')
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
if ($userTenants->isEmpty()) {
|
|
return [
|
|
'user' => $userInfo,
|
|
'tenant' => null,
|
|
'menus' => [],
|
|
'roles' => [],
|
|
];
|
|
}
|
|
|
|
$defaultUserTenant = $userTenants->first();
|
|
$tenant = $defaultUserTenant->tenant;
|
|
|
|
// 2-1. 소속 부서 조회 (tenant_user_profiles → departments)
|
|
$profile = DB::table('tenant_user_profiles')
|
|
->where('user_id', $userId)
|
|
->where('tenant_id', $tenant->id)
|
|
->first();
|
|
if ($profile && $profile->department_id) {
|
|
$dept = DB::table('departments')->where('id', $profile->department_id)->first();
|
|
if ($dept) {
|
|
$userInfo['department'] = $dept->name;
|
|
}
|
|
}
|
|
|
|
// 3. 테넌트 정보 구성
|
|
$tenantInfo = [
|
|
'id' => $tenant->id,
|
|
'company_name' => $tenant->company_name,
|
|
'business_num' => $tenant->business_num,
|
|
'tenant_st_code' => $tenant->tenant_st_code,
|
|
'other_tenants' => $userTenants->skip(1)->map(function ($ut) {
|
|
return [
|
|
'tenant_id' => $ut->tenant_id,
|
|
'company_name' => $ut->tenant->company_name,
|
|
'business_num' => $ut->tenant->business_num,
|
|
'tenant_st_code' => $ut->tenant->tenant_st_code,
|
|
];
|
|
})->values()->toArray(),
|
|
];
|
|
|
|
// 4. 메뉴 권한 체크 (mng UserPermissionService와 동일한 로직)
|
|
$now = now();
|
|
|
|
// 4-1. 역할 권한 (user_roles 테이블)
|
|
$rolePermissions = DB::table('user_roles')
|
|
->join('role_has_permissions', 'user_roles.role_id', '=', 'role_has_permissions.role_id')
|
|
->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id')
|
|
->where('user_roles.user_id', $userId)
|
|
->where('user_roles.tenant_id', $tenant->id)
|
|
->whereNull('user_roles.deleted_at')
|
|
->where('permissions.name', 'like', 'menu:%.view')
|
|
->pluck('permissions.name')
|
|
->toArray();
|
|
|
|
// 4-2. 부서 권한 (permission_overrides에서 Department 타입, effect=1)
|
|
$deptPermissions = DB::table('department_user')
|
|
->join('permission_overrides', function ($join) use ($now) {
|
|
$join->on('permission_overrides.model_id', '=', 'department_user.department_id')
|
|
->where('permission_overrides.model_type', '=', Department::class)
|
|
->whereNull('permission_overrides.deleted_at')
|
|
->where('permission_overrides.effect', 1)
|
|
->where(function ($q) use ($now) {
|
|
$q->whereNull('permission_overrides.effective_from')
|
|
->orWhere('permission_overrides.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($q) use ($now) {
|
|
$q->whereNull('permission_overrides.effective_to')
|
|
->orWhere('permission_overrides.effective_to', '>=', $now);
|
|
});
|
|
})
|
|
->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id')
|
|
->whereNull('department_user.deleted_at')
|
|
->where('department_user.user_id', $userId)
|
|
->where('department_user.tenant_id', $tenant->id)
|
|
->where('permission_overrides.tenant_id', $tenant->id)
|
|
->where('permissions.name', 'like', 'menu:%.view')
|
|
->pluck('permissions.name')
|
|
->toArray();
|
|
|
|
// 4-3. 개인 ALLOW 권한 (permission_overrides에서 User 타입, effect=1)
|
|
// Note: mng는 App\Models\User를 사용하므로 하드코딩
|
|
$personalAllows = DB::table('permission_overrides')
|
|
->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id')
|
|
->where('permission_overrides.model_type', 'App\\Models\\User')
|
|
->where('permission_overrides.model_id', $userId)
|
|
->where('permission_overrides.tenant_id', $tenant->id)
|
|
->where('permission_overrides.effect', 1)
|
|
->whereNull('permission_overrides.deleted_at')
|
|
->where(function ($q) use ($now) {
|
|
$q->whereNull('permission_overrides.effective_from')
|
|
->orWhere('permission_overrides.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($q) use ($now) {
|
|
$q->whereNull('permission_overrides.effective_to')
|
|
->orWhere('permission_overrides.effective_to', '>=', $now);
|
|
})
|
|
->where('permissions.name', 'like', 'menu:%.view')
|
|
->pluck('permissions.name')
|
|
->toArray();
|
|
|
|
// 4-4. 개인 DENY 권한 (permission_overrides에서 User 타입, effect=0)
|
|
// Note: mng는 App\Models\User를 사용하므로 하드코딩
|
|
$personalDenies = DB::table('permission_overrides')
|
|
->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id')
|
|
->where('permission_overrides.model_type', 'App\\Models\\User')
|
|
->where('permission_overrides.model_id', $userId)
|
|
->where('permission_overrides.tenant_id', $tenant->id)
|
|
->where('permission_overrides.effect', 0)
|
|
->whereNull('permission_overrides.deleted_at')
|
|
->where(function ($q) use ($now) {
|
|
$q->whereNull('permission_overrides.effective_from')
|
|
->orWhere('permission_overrides.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($q) use ($now) {
|
|
$q->whereNull('permission_overrides.effective_to')
|
|
->orWhere('permission_overrides.effective_to', '>=', $now);
|
|
})
|
|
->where('permissions.name', 'like', 'menu:%.view')
|
|
->pluck('permissions.name')
|
|
->toArray();
|
|
|
|
// 4-5. 최종 권한 계산: (역할 OR 부서 OR 개인ALLOW) - 개인DENY
|
|
$allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows));
|
|
$effectivePermissions = array_diff($allAllowed, $personalDenies);
|
|
|
|
// 메뉴 ID 추출
|
|
$allowedMenuIds = [];
|
|
foreach ($effectivePermissions as $permName) {
|
|
if (preg_match('/^menu:(\d+)\.view$/', $permName, $matches)) {
|
|
$allowedMenuIds[] = (int) $matches[1];
|
|
}
|
|
}
|
|
|
|
// 5. 메뉴 목록 조회 (권한 있는 메뉴만)
|
|
$menus = [];
|
|
if (! empty($allowedMenuIds)) {
|
|
$menus = Menu::where('tenant_id', $tenant->id)
|
|
->where('is_active', 1)
|
|
->whereIn('id', $allowedMenuIds)
|
|
->orderBy('parent_id')
|
|
->orderBy('sort_order')
|
|
->get(['id', 'parent_id', 'name', 'url', 'icon', 'sort_order', 'is_external', 'external_url'])
|
|
->toArray();
|
|
}
|
|
|
|
// 6. 역할(Role) 정보 조회 (user_roles 테이블 사용 - mng와 동일)
|
|
$roles = DB::table('user_roles')
|
|
->join('roles', 'user_roles.role_id', '=', 'roles.id')
|
|
->where('user_roles.user_id', $userId)
|
|
->where('user_roles.tenant_id', $tenant->id)
|
|
->whereNull('user_roles.deleted_at')
|
|
->select('roles.id', 'roles.name', 'roles.description')
|
|
->get()
|
|
->toArray();
|
|
|
|
return [
|
|
'user' => $userInfo,
|
|
'tenant' => $tenantInfo,
|
|
'menus' => $menus,
|
|
'roles' => $roles,
|
|
];
|
|
}
|
|
}
|