fix : 메뉴 모델 및 일부 서비스파일 response 오류 수정

This commit is contained in:
2025-08-16 04:16:34 +09:00
parent 73d06e03b0
commit 6f1842181e
5 changed files with 312 additions and 211 deletions

View File

@@ -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);
}
});
}
}

View File

@@ -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');

View File

@@ -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');
}
}

View File

@@ -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(),
];
]);
}
}

View File

@@ -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());
}
}