diff --git a/app/Models/Commons/Menu.php b/app/Models/Commons/Menu.php index ecbb15b..8dd3468 100644 --- a/app/Models/Commons/Menu.php +++ b/app/Models/Commons/Menu.php @@ -4,14 +4,25 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use App\Traits\BelongsToTenant; +use App\Traits\ModelTrait; +use App\Models\Scopes\TenantScope; class Menu extends Model { - use SoftDeletes; + use SoftDeletes, BelongsToTenant, ModelTrait; protected $fillable = [ - 'tenant_id', 'parent_id', 'slug', 'name', 'url', 'is_active', 'sort_order', - 'hidden', 'is_external', 'external_url', 'icon' + 'tenant_id', 'parent_id', 'name', 'url', 'is_active', 'sort_order', + 'hidden', 'is_external', 'external_url', 'icon', + 'created_by', 'updated_by', 'deleted_by', + ]; + + protected $hidden = [ + 'created_by', + 'updated_by', + 'deleted_by', + 'deleted_at' ]; public function parent() @@ -23,4 +34,23 @@ public function children() { return $this->hasMany(Menu::class, 'parent_id'); } + + /** + * 공유(NULL) + 현재 테넌트 모두 포함해서 조회 + * (SoftDeletes 글로벌 스코프는 그대로 유지) + */ + public function scopeWithShared($query, ?int $tenantId = null) + { + $tenantId = $tenantId ?? app('tenant_id'); + + return $query + ->withoutGlobalScope(TenantScope::class) + ->where(function ($w) use ($tenantId) { + if (is_null($tenantId)) { + $w->whereNull('tenant_id'); + } else { + $w->whereNull('tenant_id')->orWhere('tenant_id', $tenantId); + } + }); + } } diff --git a/app/Services/Authz/RoleService.php b/app/Services/Authz/RoleService.php index 8a475de..58d65e5 100644 --- a/app/Services/Authz/RoleService.php +++ b/app/Services/Authz/RoleService.php @@ -2,12 +2,12 @@ namespace App\Services\Authz; -use Illuminate\Validation\Rule; -use Illuminate\Support\Facades\Validator; +use App\Helpers\ApiResponse; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; use Spatie\Permission\Models\Role; use Spatie\Permission\PermissionRegistrar; -use App\Helpers\ApiResponse; class RoleService { @@ -26,13 +26,13 @@ public static function index(array $params = []) ->where('guard_name', self::$guard); if ($q !== '') { - $query->where(function($w) use ($q) { + $query->where(function ($w) use ($q) { $w->where('name', 'like', "%{$q}%") ->orWhere('description', 'like', "%{$q}%"); }); } - $list = $query->orderBy('id','desc') + $list = $query->orderByDesc('id') ->paginate($size, ['*'], 'page', $page); return ApiResponse::response('result', $list); @@ -44,10 +44,10 @@ public static function store(array $params = []) $tenantId = (int) app('tenant_id'); $v = Validator::make($params, [ - 'name' => [ - 'required','string','max:100', - Rule::unique('roles','name')->where(fn($q)=>$q - ->where('tenant_id',$tenantId) + 'name' => [ + 'required', 'string', 'max:100', + Rule::unique('roles', 'name')->where(fn($q) => $q + ->where('tenant_id', $tenantId) ->where('guard_name', self::$guard)), ], 'description' => 'nullable|string|max:255', @@ -57,13 +57,14 @@ public static function store(array $params = []) return ApiResponse::error($v->errors()->first(), 422); } + // Spatie 팀(테넌트) 컨텍스트 app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId); $role = Role::create([ - 'tenant_id' => $tenantId, - 'guard_name' => self::$guard, - 'name' => $v->validated()['name'], - 'description'=> $params['description'] ?? null, + 'tenant_id' => $tenantId, + 'guard_name' => self::$guard, + 'name' => $v->validated()['name'], + 'description' => $params['description'] ?? null, ]); return ApiResponse::response('result', $role); @@ -90,7 +91,7 @@ public static function update(int $id, array $params = []) { $tenantId = (int) app('tenant_id'); - $role = Role::where('tenant_id',$tenantId) + $role = Role::where('tenant_id', $tenantId) ->where('guard_name', self::$guard) ->find($id); @@ -112,18 +113,17 @@ public static function update(int $id, array $params = []) return ApiResponse::error($v->errors()->first(), 422); } - $payload = $v->validated(); - $role->fill($payload)->save(); + $role->fill($v->validated())->save(); - return ApiResponse::response('result', $role); + return ApiResponse::response('result', $role->fresh()); } - /** 삭제 (현재는 하드삭제) */ + /** 삭제 (하드삭제) */ public static function destroy(int $id) { $tenantId = (int) app('tenant_id'); - $role = Role::where('tenant_id',$tenantId) + $role = Role::where('tenant_id', $tenantId) ->where('guard_name', self::$guard) ->find($id); @@ -132,8 +132,7 @@ public static function destroy(int $id) } DB::transaction(function () use ($role) { - // 연관 피벗은 스파티가 onDelete cascade 하므로 기본 동작으로 OK - $role->delete(); // ※ 기본 Spatie Role은 SoftDeletes 미사용 → 하드 삭제 + $role->delete(); // Spatie Role 기본: soft delete 없음 }); return ApiResponse::response('success'); diff --git a/app/Services/Authz/UserRoleService.php b/app/Services/Authz/UserRoleService.php index 5e3531b..5592204 100644 --- a/app/Services/Authz/UserRoleService.php +++ b/app/Services/Authz/UserRoleService.php @@ -54,18 +54,14 @@ protected static function resolveRoleNames(int $tenantId, array $params): array // 정제 $names = array_values(array_unique(array_filter($names))); - // 존재하지 않는 이름이 섞였는지 확인(실패 시 422로 안내하고 싶다면 여기서 검사) + // 존재 확인(필요시 에러 처리 확장 가능) if (!empty($names)) { $count = Role::query() ->where('tenant_id', $tenantId) ->where('guard_name', self::$guard) ->whereIn('name', $names) ->count(); - - if ($count !== count($names)) { - // 존재하지 않는 역할 이름이 포함됨 - // 필요하면 어떤 이름이 없는지 찾아서 에러 반환하도록 개선 가능 - } + // if ($count !== count($names)) { ... 필요시 상세 에러 반환 } } return $names; @@ -84,13 +80,13 @@ public static function list(int $userId) self::setTeam($tenantId); // 현재 테넌트의 역할만 - $roles = $user->roles() + $builder = $user->roles() ->where('roles.tenant_id', $tenantId) ->where('roles.guard_name', self::$guard) - ->orderBy('roles.id', 'desc') - ->get(['roles.id','roles.tenant_id','roles.name','roles.description','roles.guard_name','roles.created_at','roles.updated_at']); + ->select(['roles.id','roles.tenant_id','roles.name','roles.description','roles.guard_name','roles.created_at','roles.updated_at']) + ->orderBy('roles.id', 'desc'); - return ApiResponse::response('result', $roles); + return ApiResponse::response('get', $builder); } /** 부여 (중복 무시) */ @@ -104,10 +100,10 @@ public static function grant(int $userId, array $params = []) } $v = Validator::make($params, [ - 'role_names' => 'sometimes|array', + 'role_names' => 'sometimes|array', 'role_names.*' => 'string|min:1', - 'role_ids' => 'sometimes|array', - 'role_ids.*' => 'integer|min:1', + 'role_ids' => 'sometimes|array', + 'role_ids.*' => 'integer|min:1', ]); if ($v->fails()) { return ApiResponse::error($v->errors()->first(), 422); @@ -123,9 +119,7 @@ public static function grant(int $userId, array $params = []) return ApiResponse::error('유효한 역할이 없습니다.', 422); } - // Spatie: 이름 배열로 부여 (teams 컨텍스트 적용) - $user->assignRole($names); - + $user->assignRole($names); // teams 컨텍스트 적용됨 return ApiResponse::response('success'); } @@ -140,10 +134,10 @@ public static function revoke(int $userId, array $params = []) } $v = Validator::make($params, [ - 'role_names' => 'sometimes|array', + 'role_names' => 'sometimes|array', 'role_names.*' => 'string|min:1', - 'role_ids' => 'sometimes|array', - 'role_ids.*' => 'integer|min:1', + 'role_ids' => 'sometimes|array', + 'role_ids.*' => 'integer|min:1', ]); if ($v->fails()) { return ApiResponse::error($v->errors()->first(), 422); @@ -160,7 +154,6 @@ public static function revoke(int $userId, array $params = []) } $user->removeRole($names); // 배열 허용 - return ApiResponse::response('success'); } @@ -175,10 +168,10 @@ public static function sync(int $userId, array $params = []) } $v = Validator::make($params, [ - 'role_names' => 'sometimes|array', + 'role_names' => 'sometimes|array', 'role_names.*' => 'string|min:1', - 'role_ids' => 'sometimes|array', - 'role_ids.*' => 'integer|min:1', + 'role_ids' => 'sometimes|array', + 'role_ids.*' => 'integer|min:1', ]); if ($v->fails()) { return ApiResponse::error($v->errors()->first(), 422); @@ -191,13 +184,11 @@ public static function sync(int $userId, array $params = []) $names = self::resolveRoleNames($tenantId, $params); if (empty($names)) { - // 빈 목록으로 sync = 모두 제거 의도라면 허용할 수도 있음. - // 정책에 맞춰 처리: 여기서는 빈 목록이면 실패 처리 + // 정책상 빈 목록 sync 허용 시: $user->syncRoles([]) 로 전부 제거 가능 return ApiResponse::error('유효한 역할이 없습니다.', 422); } - $user->syncRoles($names); // 교체 - + $user->syncRoles($names); return ApiResponse::response('success'); } } diff --git a/app/Services/DepartmentService.php b/app/Services/DepartmentService.php index 906770a..7d2f0ac 100644 --- a/app/Services/DepartmentService.php +++ b/app/Services/DepartmentService.php @@ -2,19 +2,26 @@ namespace App\Services; +use App\Helpers\ApiResponse; use App\Models\Commons\Department; -use App\Models\Commons\DepartmentUser; use App\Models\Commons\DepartmentPermission; +use App\Models\Commons\DepartmentUser; +use Carbon\Carbon; +use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; -use Carbon\Carbon; class DepartmentService { + /** + * 공통 검증 헬퍼: 실패 시 JsonResponse 반환 + */ private static function v(array $params, array $rules) { $v = Validator::make($params, $rules); - if ($v->fails()) abort(422, $v->errors()->first()); + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } return $v->validated(); } @@ -27,18 +34,27 @@ public static function index(array $params) 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:200', ]); + if ($p instanceof JsonResponse) return $p; $q = Department::query(); - if (isset($p['is_active'])) $q->where('is_active', (int)$p['is_active']); + if (isset($p['is_active'])) { + $q->where('is_active', (int)$p['is_active']); + } if (!empty($p['q'])) { - $q->where(function($w) use ($p) { - $w->where('name','like','%'.$p['q'].'%') - ->orWhere('code','like','%'.$p['q'].'%'); + $q->where(function ($w) use ($p) { + $w->where('name', 'like', '%' . $p['q'] . '%') + ->orWhere('code', 'like', '%' . $p['q'] . '%'); }); } - return $q->orderBy('sort_order')->orderBy('name')->paginate($p['per_page'] ?? 20); + $q->orderBy('sort_order')->orderBy('name'); + + $perPage = $p['per_page'] ?? 20; + $page = $p['page'] ?? null; + + // 페이징 객체는 'result'로 반환 + return ApiResponse::response('result', $q->paginate($perPage, ['*'], 'page', $page)); } /** 생성 */ @@ -52,10 +68,11 @@ public static function store(array $params) 'sort_order' => 'nullable|integer', 'created_by' => 'nullable|integer|min:1', ]); + if ($p instanceof JsonResponse) return $p; if (!empty($p['code'])) { $exists = Department::query()->where('code', $p['code'])->exists(); - if ($exists) abort(409, '이미 존재하는 부서 코드입니다.'); + if ($exists) return ApiResponse::error('이미 존재하는 부서 코드입니다.', 409); } $dept = Department::create([ @@ -68,20 +85,28 @@ public static function store(array $params) 'updated_by' => $p['created_by'] ?? null, ]); - return self::show($dept->id, []); + return ApiResponse::response('result', $dept->fresh()); } /** 단건 */ public static function show(int $id, array $params) { - $dept = Department::query()->find($id); - if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); - return $dept; + if (!$id) return ApiResponse::error('id가 필요합니다.', 400); + + $q = Department::query()->where('id', $id); + $res = ApiResponse::response('first', $q); + + if (empty($res['data'])) { + return ApiResponse::error('부서를 찾을 수 없습니다.', 404); + } + return $res; } /** 수정 */ public static function update(int $id, array $params) { + if (!$id) return ApiResponse::error('id가 필요합니다.', 400); + $p = self::v($params, [ 'code' => 'nullable|string|max:50', 'name' => 'nullable|string|max:100', @@ -90,16 +115,17 @@ public static function update(int $id, array $params) 'sort_order' => 'nullable|integer', 'updated_by' => 'nullable|integer|min:1', ]); + if ($p instanceof JsonResponse) return $p; $dept = Department::query()->find($id); - if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); + if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404); if (array_key_exists('code', $p) && !is_null($p['code'])) { $exists = Department::query() ->where('code', $p['code']) - ->where('id','!=',$id) + ->where('id', '!=', $id) ->exists(); - if ($exists) abort(409, '이미 존재하는 부서 코드입니다.'); + if ($exists) return ApiResponse::error('이미 존재하는 부서 코드입니다.', 409); } $dept->fill([ @@ -111,18 +137,21 @@ public static function update(int $id, array $params) 'updated_by' => $p['updated_by'] ?? $dept->updated_by, ])->save(); - return $dept->fresh(); + return ApiResponse::response('result', $dept->fresh()); } /** 삭제(soft) */ public static function destroy(int $id, array $params) { + if (!$id) return ApiResponse::error('id가 필요합니다.', 400); + $p = self::v($params, [ 'deleted_by' => 'nullable|integer|min:1', ]); + if ($p instanceof JsonResponse) return $p; $dept = Department::query()->find($id); - if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); + if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404); if (!empty($p['deleted_by'])) { $dept->deleted_by = $p['deleted_by']; @@ -130,7 +159,7 @@ public static function destroy(int $id, array $params) } $dept->delete(); - return ['id'=>$id, 'deleted_at'=>now()->toDateTimeString()]; + return ApiResponse::response('result', ['id' => $id, 'deleted_at' => now()->toDateTimeString()]); } /** 부서 사용자 목록 */ @@ -140,15 +169,18 @@ public static function listUsers(int $deptId, array $params) 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:200', ]); + if ($p instanceof JsonResponse) return $p; $dept = Department::query()->find($deptId); - if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); + if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404); - return $dept->departmentUsers() - ->with('user') - ->orderByDesc('is_primary') - ->orderBy('id') - ->paginate($p['per_page'] ?? 20); + $builder = $dept->departmentUsers()->with('user') + ->orderByDesc('is_primary')->orderBy('id'); + + $perPage = $p['per_page'] ?? 20; + $page = $p['page'] ?? null; + + return ApiResponse::response('result', $builder->paginate($perPage, ['*'], 'page', $page)); } /** 사용자 배정 (단건) */ @@ -159,24 +191,25 @@ public static function attachUser(int $deptId, array $params) 'is_primary' => 'nullable|in:0,1', 'joined_at' => 'nullable|date', ]); + if ($p instanceof JsonResponse) return $p; $dept = Department::query()->find($deptId); - if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); + if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404); - return DB::transaction(function () use ($dept, $p) { + $result = DB::transaction(function () use ($dept, $p) { $du = DepartmentUser::withTrashed() ->where('department_id', $dept->id) ->where('user_id', $p['user_id']) ->first(); if ($du && is_null($du->deleted_at)) { - abort(409, '이미 배정된 사용자입니다.'); + return ApiResponse::error('이미 배정된 사용자입니다.', 409); } if (!empty($p['is_primary']) && (int)$p['is_primary'] === 1) { DepartmentUser::whereNull('deleted_at') ->where('user_id', $p['user_id']) - ->update(['is_primary'=>0]); + ->update(['is_primary' => 0]); } $payload = [ @@ -194,8 +227,13 @@ public static function attachUser(int $deptId, array $params) DepartmentUser::create($payload); } - return ['department_id'=>$dept->id, 'user_id'=>$p['user_id']]; + return ['department_id' => $dept->id, 'user_id' => $p['user_id']]; }); + + // 트랜잭션 내부에서 에러 응답이 나올 수 있으므로 분기 + if ($result instanceof JsonResponse) return $result; + + return ApiResponse::response('result', $result); } /** 사용자 제거(soft) */ @@ -206,10 +244,14 @@ public static function detachUser(int $deptId, int $userId, array $params) ->where('user_id', $userId) ->first(); - if (!$du) abort(404, '배정된 사용자를 찾을 수 없습니다.'); + if (!$du) return ApiResponse::error('배정된 사용자를 찾을 수 없습니다.', 404); + $du->delete(); - return ['user_id'=>$userId, 'deleted_at'=>now()->toDateTimeString()]; + return ApiResponse::response('result', [ + 'user_id' => $userId, + 'deleted_at' => now()->toDateTimeString(), + ]); } /** 주부서 설정/해제 */ @@ -218,14 +260,17 @@ public static function setPrimary(int $deptId, int $userId, array $params) $p = self::v($params, [ 'is_primary' => 'required|in:0,1', ]); + if ($p instanceof JsonResponse) return $p; - return DB::transaction(function () use ($deptId, $userId, $p) { + $result = DB::transaction(function () use ($deptId, $userId, $p) { $du = DepartmentUser::whereNull('deleted_at') ->where('department_id', $deptId) ->where('user_id', $userId) ->first(); - if (!$du) abort(404, '배정된 사용자를 찾을 수 없습니다.'); + if (!$du) { + return ApiResponse::error('배정된 사용자를 찾을 수 없습니다.', 404); + } if ((int)$p['is_primary'] === 1) { DepartmentUser::whereNull('deleted_at') @@ -236,8 +281,12 @@ public static function setPrimary(int $deptId, int $userId, array $params) $du->is_primary = (int)$p['is_primary']; $du->save(); - return ['user_id'=>$userId,'department_id'=>$deptId,'is_primary'=>$du->is_primary]; + return ['user_id' => $userId, 'department_id' => $deptId, 'is_primary' => $du->is_primary]; }); + + if ($result instanceof JsonResponse) return $result; + + return ApiResponse::response('result', $result); } /** 부서 권한 목록 */ @@ -249,9 +298,10 @@ public static function listPermissions(int $deptId, array $params) 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:200', ]); + if ($p instanceof JsonResponse) return $p; $dept = Department::query()->find($deptId); - if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); + if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404); $q = DepartmentPermission::query() ->whereNull('deleted_at') @@ -260,7 +310,12 @@ public static function listPermissions(int $deptId, array $params) if (isset($p['menu_id'])) $q->where('menu_id', $p['menu_id']); if (isset($p['is_allowed'])) $q->where('is_allowed', (int)$p['is_allowed']); - return $q->orderByDesc('is_allowed')->orderBy('permission_id')->paginate($p['per_page'] ?? 20); + $q->orderByDesc('is_allowed')->orderBy('permission_id'); + + $perPage = $p['per_page'] ?? 20; + $page = $p['page'] ?? null; + + return ApiResponse::response('result', $q->paginate($perPage, ['*'], 'page', $page)); } /** 권한 부여/차단 upsert */ @@ -271,9 +326,10 @@ public static function upsertPermission(int $deptId, array $params) 'menu_id' => 'nullable|integer|min:1', 'is_allowed' => 'nullable|in:0,1', ]); + if ($p instanceof JsonResponse) return $p; $dept = Department::query()->find($deptId); - if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); + if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404); $payload = [ 'department_id' => $deptId, @@ -286,6 +342,7 @@ public static function upsertPermission(int $deptId, array $params) $model->deleted_at = null; $model->save(); + // 변경 후 목록 반환 return self::listPermissions($deptId, []); } @@ -295,6 +352,7 @@ public static function revokePermission(int $deptId, int $permissionId, array $p $p = self::v($params, [ 'menu_id' => 'nullable|integer|min:1', ]); + if ($p instanceof JsonResponse) return $p; $q = DepartmentPermission::whereNull('deleted_at') ->where('department_id', $deptId) @@ -303,14 +361,14 @@ public static function revokePermission(int $deptId, int $permissionId, array $p if (isset($p['menu_id'])) $q->where('menu_id', $p['menu_id']); $rows = $q->get(); - if ($rows->isEmpty()) abort(404, '대상 권한을 찾을 수 없습니다.'); + if ($rows->isEmpty()) return ApiResponse::error('대상 권한을 찾을 수 없습니다.', 404); foreach ($rows as $row) $row->delete(); - return [ + return ApiResponse::response('result', [ 'permission_id' => $permissionId, 'menu_id' => $p['menu_id'] ?? null, 'deleted_count' => $rows->count(), - ]; + ]); } } diff --git a/app/Services/MenuService.php b/app/Services/MenuService.php index e3569db..28b6423 100644 --- a/app/Services/MenuService.php +++ b/app/Services/MenuService.php @@ -2,67 +2,77 @@ namespace App\Services; -use Illuminate\Support\Facades\DB; +use App\Models\Commons\Menu; +use App\Helpers\ApiResponse; use Illuminate\Support\Arr; -use Illuminate\Validation\ValidationException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; class MenuService { - protected static function tenantId(array $params): ?int + protected static function tenantId(): ?int { - return $params['tenant_id'] ?? request()->attributes->get('tenant_id'); + return app('tenant_id'); } - protected static function actorId(array $params): ?int + protected static function actorId(): ?int { - return $params['user_id'] ?? (request()->user()->id ?? null); + $user = app('api_user'); // 컨테이너에 주입된 인증 사용자(객체 or 배열) + return is_object($user) ? ($user->id ?? null) : ($user['id'] ?? null); } + /** + * 메뉴 목록 조회 + */ public static function index(array $params) { - $tenantId = self::tenantId($params); + $tenantId = self::tenantId(); - $q = DB::table('menus')->whereNull('deleted_at'); - if (!is_null($tenantId)) { - $q->where(function ($w) use ($tenantId) { - $w->whereNull('tenant_id')->orWhere('tenant_id', $tenantId); - }); - } - // 옵션: parent_id / is_active / hidden 필터 - if (isset($params['parent_id'])) $q->where('parent_id', $params['parent_id']); - if (isset($params['is_active'])) $q->where('is_active', (int)$params['is_active']); - if (isset($params['hidden'])) $q->where('hidden', (int)$params['hidden']); + $q = Menu::query()->withShared($tenantId); - return $q->orderBy('parent_id')->orderBy('sort_order')->get(); + if (array_key_exists('parent_id', $params)) $q->where('parent_id', $params['parent_id']); + if (array_key_exists('is_active', $params)) $q->where('is_active', (int)$params['is_active']); + if (array_key_exists('hidden', $params)) $q->where('hidden', (int)$params['hidden']); + + $q->orderBy('parent_id')->orderBy('sort_order'); + + // Builder 그대로 전달해야 쿼리로그/표준응답 형식 유지 + return ApiResponse::response('get', $q); } + /** + * 메뉴 단건 조회 + */ public static function show(array $params) { - $id = (int)($params['id'] ?? 0); - $tenantId = self::tenantId($params); + $id = (int)($params['id'] ?? 0); + $tenantId = self::tenantId(); - $q = DB::table('menus')->where('id', $id)->whereNull('deleted_at'); - if (!is_null($tenantId)) { - $q->where(function ($w) use ($tenantId) { - $w->whereNull('tenant_id')->orWhere('tenant_id', $tenantId); - }); + if (!$id) { + return ApiResponse::error('id가 필요합니다.', 400); } - $row = $q->first(); - throw_if(!$row, ValidationException::withMessages(['id' => 'Menu not found'])); - return $row; + $q = Menu::withShared($tenantId)->where('id', $id); + + // first 쿼리를 ApiResponse에 위임 (존재X면 null 반환) + $res = ApiResponse::response('first', $q); + if (empty($res['data'])) { + return ApiResponse::error('Menu not found', 404); + } + return $res; } + /** + * 메뉴 생성 + */ public static function store(array $params) { - $tenantId = self::tenantId($params); - $userId = self::actorId($params); + $tenantId = self::tenantId(); + $userId = self::actorId(); $v = Validator::make($params, [ 'parent_id' => ['nullable','integer'], 'name' => ['required','string','max:100'], - 'slug' => ['nullable','string','max:150'], 'url' => ['nullable','string','max:255'], 'is_active' => ['nullable','boolean'], 'sort_order' => ['nullable','integer'], @@ -71,52 +81,47 @@ public static function store(array $params) 'external_url' => ['nullable','string','max:255'], 'icon' => ['nullable','string','max:50'], ]); + + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } $data = $v->validated(); - // slug 유니크(테넌트 범위) 체크 - if (!empty($data['slug'])) { - $exists = DB::table('menus') - ->whereNull('deleted_at') - ->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) - ->where('slug',$data['slug']) - ->exists(); - if ($exists) { - throw ValidationException::withMessages(['slug'=>'이미 사용 중인 슬러그입니다.']); - } - } + $menu = new Menu(); + $menu->tenant_id = $tenantId; + $menu->parent_id = $data['parent_id'] ?? null; + $menu->name = $data['name']; + $menu->url = $data['url'] ?? null; + $menu->is_active = (int)($data['is_active'] ?? 1); + $menu->sort_order = (int)($data['sort_order'] ?? 0); + $menu->hidden = (int)($data['hidden'] ?? 0); + $menu->is_external = (int)($data['is_external'] ?? 0); + $menu->external_url = $data['external_url'] ?? null; + $menu->icon = $data['icon'] ?? null; + $menu->created_by = $userId; + $menu->updated_by = $userId; + $menu->save(); - $now = now(); - $id = DB::table('menus')->insertGetId([ - 'tenant_id' => $tenantId, - 'parent_id' => $data['parent_id'] ?? null, - 'name' => $data['name'], - 'slug' => $data['slug'] ?? null, - 'url' => $data['url'] ?? null, - 'is_active' => (int)($data['is_active'] ?? 1), - 'sort_order' => (int)($data['sort_order'] ?? 0), - 'hidden' => (int)($data['hidden'] ?? 0), - 'is_external' => (int)($data['is_external'] ?? 0), - 'external_url' => $data['external_url'] ?? null, - 'icon' => $data['icon'] ?? null, - 'created_at' => $now, - 'updated_at' => $now, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); - - return ['id' => $id]; + // 생성 결과를 그대로 전달 + return ApiResponse::response('result', $menu->fresh()); } + /** + * 메뉴 수정 + */ public static function update(array $params) { $id = (int)($params['id'] ?? 0); - $tenantId = self::tenantId($params); - $userId = self::actorId($params); + $tenantId = self::tenantId(); + $userId = self::actorId(); + + if (!$id) { + return ApiResponse::error('id가 필요합니다.', 400); + } $v = Validator::make($params, [ 'parent_id' => ['nullable','integer'], 'name' => ['nullable','string','max:100'], - 'slug' => ['nullable','string','max:150'], 'url' => ['nullable','string','max:255'], 'is_active' => ['nullable','boolean'], 'sort_order' => ['nullable','integer'], @@ -125,50 +130,55 @@ public static function update(array $params) 'external_url' => ['nullable','string','max:255'], 'icon' => ['nullable','string','max:50'], ]); + + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } $data = $v->validated(); - // 대상 존재 확인 & 테넌트 범위 - $exists = DB::table('menus')->where('id',$id)->whereNull('deleted_at') - ->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) - ->exists(); - if (!$exists) throw ValidationException::withMessages(['id'=>'Menu not found']); - - // slug 유니크(테넌트 범위) 체크 - if (!empty($data['slug'])) { - $dup = DB::table('menus')->whereNull('deleted_at') - ->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) - ->where('slug',$data['slug'])->where('id','<>',$id)->exists(); - if ($dup) throw ValidationException::withMessages(['slug'=>'이미 사용 중인 슬러그입니다.']); + $menu = Menu::withShared($tenantId)->where('id', $id)->first(); + if (!$menu) { + return ApiResponse::error('Menu not found', 404); } - $update = Arr::only($data, ['parent_id','name','slug','url','is_active','sort_order','hidden','is_external','external_url','icon']); - $update = array_filter($update, fn($v)=>!is_null($v)); - $update['updated_at'] = now(); - $update['updated_by'] = $userId; + $update = Arr::only($data, [ + 'parent_id','name','url','is_active','sort_order','hidden','is_external','external_url','icon' + ]); + $update = array_filter($update, fn($v) => !is_null($v)); - DB::table('menus')->where('id',$id)->update($update); - return ['id' => $id]; + if (empty($update)) { + return ApiResponse::error('수정할 데이터가 없습니다.', 400); + } + + $update['updated_by'] = $userId; + $menu->fill($update)->save(); + + return ApiResponse::response('result', $menu->fresh()); } + /** + * 메뉴 삭제(소프트) + */ public static function destroy(array $params) { $id = (int)($params['id'] ?? 0); - $tenantId = self::tenantId($params); - $userId = self::actorId($params); + $tenantId = self::tenantId(); + $userId = self::actorId(); - $q = DB::table('menus')->where('id',$id)->whereNull('deleted_at'); - $q = !is_null($tenantId) - ? $q->where('tenant_id',$tenantId) - : $q->whereNull('tenant_id'); + if (!$id) { + return ApiResponse::error('id가 필요합니다.', 400); + } - $row = $q->first(); - if (!$row) throw ValidationException::withMessages(['id'=>'Menu not found']); + $menu = Menu::withShared($tenantId)->where('id', $id)->first(); + if (!$menu) { + return ApiResponse::error('Menu not found', 404); + } - DB::table('menus')->where('id',$id)->update([ - 'deleted_at' => now(), - 'deleted_by' => $userId, - ]); - return ['id' => $id, 'deleted' => true]; + $menu->deleted_by = $userId; + $menu->save(); + $menu->delete(); + + return ApiResponse::response('success'); } /** @@ -178,43 +188,56 @@ public static function destroy(array $params) public static function reorder(array $params) { if (!is_array($params) || empty($params)) { - throw ValidationException::withMessages(['items'=>'유효한 정렬 목록이 필요합니다.']); + return ApiResponse::error('유효한 정렬 목록이 필요합니다.', 422); } - DB::transaction(function () use ($params) { + $tenantId = self::tenantId(); + + DB::transaction(function () use ($params, $tenantId) { foreach ($params as $it) { if (!isset($it['id'], $it['sort_order'])) continue; - DB::table('menus')->where('id',(int)$it['id'])->update([ - 'sort_order' => (int)$it['sort_order'], - 'updated_at' => now(), - ]); + + $menu = Menu::withShared($tenantId)->find((int)$it['id']); + if ($menu) { + $menu->sort_order = (int)$it['sort_order']; + $menu->save(); + } } }); - return true; + + return ApiResponse::response('success'); } /** - * 상태 토글 - * 허용 필드: is_active / hidden / is_external + * 상태 토글: is_active / hidden / is_external */ public static function toggle(array $params) { - $id = (int)($params['id'] ?? 0); - $userId = self::actorId($params); + $id = (int)($params['id'] ?? 0); + $tenantId = self::tenantId(); + $userId = self::actorId(); - $payload = array_filter([ - 'is_active' => isset($params['is_active']) ? (int)$params['is_active'] : null, - 'hidden' => isset($params['hidden']) ? (int)$params['hidden'] : null, - 'is_external' => isset($params['is_external']) ? (int)$params['is_external'] : null, - ], fn($v)=>!is_null($v)); - - if (empty($payload)) { - throw ValidationException::withMessages(['toggle'=>'변경할 필드가 없습니다.']); + if (!$id) { + return ApiResponse::error('id가 필요합니다.', 400); } - $payload['updated_at'] = now(); - $payload['updated_by'] = $userId; + $payload = array_filter([ + 'is_active' => array_key_exists('is_active', $params) ? (int)$params['is_active'] : null, + 'hidden' => array_key_exists('hidden', $params) ? (int)$params['hidden'] : null, + 'is_external' => array_key_exists('is_external', $params) ? (int)$params['is_external'] : null, + ], fn($v) => !is_null($v)); - DB::table('menus')->where('id',$id)->update($payload); - return ['id' => $id]; + if (empty($payload)) { + return ApiResponse::error('변경할 필드가 없습니다.', 422); + } + + $menu = Menu::withShared($tenantId)->find($id); + if (!$menu) { + return ApiResponse::error('Menu not found', 404); + } + + $payload['updated_by'] = $userId; + $menu->fill($payload)->save(); + + return ApiResponse::response('result', $menu->fresh()); } }