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\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use App\Models\Scopes\TenantScope;
class Menu extends Model class Menu extends Model
{ {
use SoftDeletes; use SoftDeletes, BelongsToTenant, ModelTrait;
protected $fillable = [ protected $fillable = [
'tenant_id', 'parent_id', 'slug', 'name', 'url', 'is_active', 'sort_order', 'tenant_id', 'parent_id', 'name', 'url', 'is_active', 'sort_order',
'hidden', 'is_external', 'external_url', 'icon' '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() public function parent()
@@ -23,4 +34,23 @@ public function children()
{ {
return $this->hasMany(Menu::class, 'parent_id'); 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; namespace App\Services\Authz;
use Illuminate\Validation\Rule; use App\Helpers\ApiResponse;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\PermissionRegistrar;
use App\Helpers\ApiResponse;
class RoleService class RoleService
{ {
@@ -26,13 +26,13 @@ public static function index(array $params = [])
->where('guard_name', self::$guard); ->where('guard_name', self::$guard);
if ($q !== '') { if ($q !== '') {
$query->where(function($w) use ($q) { $query->where(function ($w) use ($q) {
$w->where('name', 'like', "%{$q}%") $w->where('name', 'like', "%{$q}%")
->orWhere('description', 'like', "%{$q}%"); ->orWhere('description', 'like', "%{$q}%");
}); });
} }
$list = $query->orderBy('id','desc') $list = $query->orderByDesc('id')
->paginate($size, ['*'], 'page', $page); ->paginate($size, ['*'], 'page', $page);
return ApiResponse::response('result', $list); return ApiResponse::response('result', $list);
@@ -44,10 +44,10 @@ public static function store(array $params = [])
$tenantId = (int) app('tenant_id'); $tenantId = (int) app('tenant_id');
$v = Validator::make($params, [ $v = Validator::make($params, [
'name' => [ 'name' => [
'required','string','max:100', 'required', 'string', 'max:100',
Rule::unique('roles','name')->where(fn($q)=>$q Rule::unique('roles', 'name')->where(fn($q) => $q
->where('tenant_id',$tenantId) ->where('tenant_id', $tenantId)
->where('guard_name', self::$guard)), ->where('guard_name', self::$guard)),
], ],
'description' => 'nullable|string|max:255', 'description' => 'nullable|string|max:255',
@@ -57,13 +57,14 @@ public static function store(array $params = [])
return ApiResponse::error($v->errors()->first(), 422); return ApiResponse::error($v->errors()->first(), 422);
} }
// Spatie 팀(테넌트) 컨텍스트
app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId); app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId);
$role = Role::create([ $role = Role::create([
'tenant_id' => $tenantId, 'tenant_id' => $tenantId,
'guard_name' => self::$guard, 'guard_name' => self::$guard,
'name' => $v->validated()['name'], 'name' => $v->validated()['name'],
'description'=> $params['description'] ?? null, 'description' => $params['description'] ?? null,
]); ]);
return ApiResponse::response('result', $role); return ApiResponse::response('result', $role);
@@ -90,7 +91,7 @@ public static function update(int $id, array $params = [])
{ {
$tenantId = (int) app('tenant_id'); $tenantId = (int) app('tenant_id');
$role = Role::where('tenant_id',$tenantId) $role = Role::where('tenant_id', $tenantId)
->where('guard_name', self::$guard) ->where('guard_name', self::$guard)
->find($id); ->find($id);
@@ -112,18 +113,17 @@ public static function update(int $id, array $params = [])
return ApiResponse::error($v->errors()->first(), 422); return ApiResponse::error($v->errors()->first(), 422);
} }
$payload = $v->validated(); $role->fill($v->validated())->save();
$role->fill($payload)->save();
return ApiResponse::response('result', $role); return ApiResponse::response('result', $role->fresh());
} }
/** 삭제 (현재는 하드삭제) */ /** 삭제 (하드삭제) */
public static function destroy(int $id) public static function destroy(int $id)
{ {
$tenantId = (int) app('tenant_id'); $tenantId = (int) app('tenant_id');
$role = Role::where('tenant_id',$tenantId) $role = Role::where('tenant_id', $tenantId)
->where('guard_name', self::$guard) ->where('guard_name', self::$guard)
->find($id); ->find($id);
@@ -132,8 +132,7 @@ public static function destroy(int $id)
} }
DB::transaction(function () use ($role) { DB::transaction(function () use ($role) {
// 연관 피벗은 스파티가 onDelete cascade 하므로 기본 동작으로 OK $role->delete(); // Spatie Role 기본: soft delete 없음
$role->delete(); // ※ 기본 Spatie Role은 SoftDeletes 미사용 → 하드 삭제
}); });
return ApiResponse::response('success'); 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))); $names = array_values(array_unique(array_filter($names)));
// 존재하지 않는 이름이 섞였는지 확인(실패 시 422로 안내하고 싶다면 여기서 검사) // 존재 확인(필요시 에러 처리 확장 가능)
if (!empty($names)) { if (!empty($names)) {
$count = Role::query() $count = Role::query()
->where('tenant_id', $tenantId) ->where('tenant_id', $tenantId)
->where('guard_name', self::$guard) ->where('guard_name', self::$guard)
->whereIn('name', $names) ->whereIn('name', $names)
->count(); ->count();
// if ($count !== count($names)) { ... 필요시 상세 에러 반환 }
if ($count !== count($names)) {
// 존재하지 않는 역할 이름이 포함됨
// 필요하면 어떤 이름이 없는지 찾아서 에러 반환하도록 개선 가능
}
} }
return $names; return $names;
@@ -84,13 +80,13 @@ public static function list(int $userId)
self::setTeam($tenantId); self::setTeam($tenantId);
// 현재 테넌트의 역할만 // 현재 테넌트의 역할만
$roles = $user->roles() $builder = $user->roles()
->where('roles.tenant_id', $tenantId) ->where('roles.tenant_id', $tenantId)
->where('roles.guard_name', self::$guard) ->where('roles.guard_name', self::$guard)
->orderBy('roles.id', 'desc') ->select(['roles.id','roles.tenant_id','roles.name','roles.description','roles.guard_name','roles.created_at','roles.updated_at'])
->get(['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, [ $v = Validator::make($params, [
'role_names' => 'sometimes|array', 'role_names' => 'sometimes|array',
'role_names.*' => 'string|min:1', 'role_names.*' => 'string|min:1',
'role_ids' => 'sometimes|array', 'role_ids' => 'sometimes|array',
'role_ids.*' => 'integer|min:1', 'role_ids.*' => 'integer|min:1',
]); ]);
if ($v->fails()) { if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422); return ApiResponse::error($v->errors()->first(), 422);
@@ -123,9 +119,7 @@ public static function grant(int $userId, array $params = [])
return ApiResponse::error('유효한 역할이 없습니다.', 422); return ApiResponse::error('유효한 역할이 없습니다.', 422);
} }
// Spatie: 이름 배열로 부여 (teams 컨텍스트 적용) $user->assignRole($names); // teams 컨텍스트 적용
$user->assignRole($names);
return ApiResponse::response('success'); return ApiResponse::response('success');
} }
@@ -140,10 +134,10 @@ public static function revoke(int $userId, array $params = [])
} }
$v = Validator::make($params, [ $v = Validator::make($params, [
'role_names' => 'sometimes|array', 'role_names' => 'sometimes|array',
'role_names.*' => 'string|min:1', 'role_names.*' => 'string|min:1',
'role_ids' => 'sometimes|array', 'role_ids' => 'sometimes|array',
'role_ids.*' => 'integer|min:1', 'role_ids.*' => 'integer|min:1',
]); ]);
if ($v->fails()) { if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422); return ApiResponse::error($v->errors()->first(), 422);
@@ -160,7 +154,6 @@ public static function revoke(int $userId, array $params = [])
} }
$user->removeRole($names); // 배열 허용 $user->removeRole($names); // 배열 허용
return ApiResponse::response('success'); return ApiResponse::response('success');
} }
@@ -175,10 +168,10 @@ public static function sync(int $userId, array $params = [])
} }
$v = Validator::make($params, [ $v = Validator::make($params, [
'role_names' => 'sometimes|array', 'role_names' => 'sometimes|array',
'role_names.*' => 'string|min:1', 'role_names.*' => 'string|min:1',
'role_ids' => 'sometimes|array', 'role_ids' => 'sometimes|array',
'role_ids.*' => 'integer|min:1', 'role_ids.*' => 'integer|min:1',
]); ]);
if ($v->fails()) { if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422); return ApiResponse::error($v->errors()->first(), 422);
@@ -191,13 +184,11 @@ public static function sync(int $userId, array $params = [])
$names = self::resolveRoleNames($tenantId, $params); $names = self::resolveRoleNames($tenantId, $params);
if (empty($names)) { if (empty($names)) {
// 빈 목록으로 sync = 모두 제거 의도라면 허용할 수도 있음. // 정책상 빈 목록 sync 허용 시: $user->syncRoles([]) 로 전부 제거 가능
// 정책에 맞춰 처리: 여기서는 빈 목록이면 실패 처리
return ApiResponse::error('유효한 역할이 없습니다.', 422); return ApiResponse::error('유효한 역할이 없습니다.', 422);
} }
$user->syncRoles($names); // 교체 $user->syncRoles($names);
return ApiResponse::response('success'); return ApiResponse::response('success');
} }
} }

View File

@@ -2,19 +2,26 @@
namespace App\Services; namespace App\Services;
use App\Helpers\ApiResponse;
use App\Models\Commons\Department; use App\Models\Commons\Department;
use App\Models\Commons\DepartmentUser;
use App\Models\Commons\DepartmentPermission; 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\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
class DepartmentService class DepartmentService
{ {
/**
* 공통 검증 헬퍼: 실패 시 JsonResponse 반환
*/
private static function v(array $params, array $rules) private static function v(array $params, array $rules)
{ {
$v = Validator::make($params, $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(); return $v->validated();
} }
@@ -27,18 +34,27 @@ public static function index(array $params)
'page' => 'nullable|integer|min:1', 'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:1|max:200', 'per_page' => 'nullable|integer|min:1|max:200',
]); ]);
if ($p instanceof JsonResponse) return $p;
$q = Department::query(); $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'])) { if (!empty($p['q'])) {
$q->where(function($w) use ($p) { $q->where(function ($w) use ($p) {
$w->where('name','like','%'.$p['q'].'%') $w->where('name', 'like', '%' . $p['q'] . '%')
->orWhere('code','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', 'sort_order' => 'nullable|integer',
'created_by' => 'nullable|integer|min:1', 'created_by' => 'nullable|integer|min:1',
]); ]);
if ($p instanceof JsonResponse) return $p;
if (!empty($p['code'])) { if (!empty($p['code'])) {
$exists = Department::query()->where('code', $p['code'])->exists(); $exists = Department::query()->where('code', $p['code'])->exists();
if ($exists) abort(409, '이미 존재하는 부서 코드입니다.'); if ($exists) return ApiResponse::error('이미 존재하는 부서 코드입니다.', 409);
} }
$dept = Department::create([ $dept = Department::create([
@@ -68,20 +85,28 @@ public static function store(array $params)
'updated_by' => $p['created_by'] ?? null, '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) public static function show(int $id, array $params)
{ {
$dept = Department::query()->find($id); if (!$id) return ApiResponse::error('id가 필요합니다.', 400);
if (!$dept) abort(404, '부서를 찾을 수 없습니다.');
return $dept; $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) public static function update(int $id, array $params)
{ {
if (!$id) return ApiResponse::error('id가 필요합니다.', 400);
$p = self::v($params, [ $p = self::v($params, [
'code' => 'nullable|string|max:50', 'code' => 'nullable|string|max:50',
'name' => 'nullable|string|max:100', 'name' => 'nullable|string|max:100',
@@ -90,16 +115,17 @@ public static function update(int $id, array $params)
'sort_order' => 'nullable|integer', 'sort_order' => 'nullable|integer',
'updated_by' => 'nullable|integer|min:1', 'updated_by' => 'nullable|integer|min:1',
]); ]);
if ($p instanceof JsonResponse) return $p;
$dept = Department::query()->find($id); $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'])) { if (array_key_exists('code', $p) && !is_null($p['code'])) {
$exists = Department::query() $exists = Department::query()
->where('code', $p['code']) ->where('code', $p['code'])
->where('id','!=',$id) ->where('id', '!=', $id)
->exists(); ->exists();
if ($exists) abort(409, '이미 존재하는 부서 코드입니다.'); if ($exists) return ApiResponse::error('이미 존재하는 부서 코드입니다.', 409);
} }
$dept->fill([ $dept->fill([
@@ -111,18 +137,21 @@ public static function update(int $id, array $params)
'updated_by' => $p['updated_by'] ?? $dept->updated_by, 'updated_by' => $p['updated_by'] ?? $dept->updated_by,
])->save(); ])->save();
return $dept->fresh(); return ApiResponse::response('result', $dept->fresh());
} }
/** 삭제(soft) */ /** 삭제(soft) */
public static function destroy(int $id, array $params) public static function destroy(int $id, array $params)
{ {
if (!$id) return ApiResponse::error('id가 필요합니다.', 400);
$p = self::v($params, [ $p = self::v($params, [
'deleted_by' => 'nullable|integer|min:1', 'deleted_by' => 'nullable|integer|min:1',
]); ]);
if ($p instanceof JsonResponse) return $p;
$dept = Department::query()->find($id); $dept = Department::query()->find($id);
if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404);
if (!empty($p['deleted_by'])) { if (!empty($p['deleted_by'])) {
$dept->deleted_by = $p['deleted_by']; $dept->deleted_by = $p['deleted_by'];
@@ -130,7 +159,7 @@ public static function destroy(int $id, array $params)
} }
$dept->delete(); $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', 'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:1|max:200', 'per_page' => 'nullable|integer|min:1|max:200',
]); ]);
if ($p instanceof JsonResponse) return $p;
$dept = Department::query()->find($deptId); $dept = Department::query()->find($deptId);
if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404);
return $dept->departmentUsers() $builder = $dept->departmentUsers()->with('user')
->with('user') ->orderByDesc('is_primary')->orderBy('id');
->orderByDesc('is_primary')
->orderBy('id') $perPage = $p['per_page'] ?? 20;
->paginate($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', 'is_primary' => 'nullable|in:0,1',
'joined_at' => 'nullable|date', 'joined_at' => 'nullable|date',
]); ]);
if ($p instanceof JsonResponse) return $p;
$dept = Department::query()->find($deptId); $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() $du = DepartmentUser::withTrashed()
->where('department_id', $dept->id) ->where('department_id', $dept->id)
->where('user_id', $p['user_id']) ->where('user_id', $p['user_id'])
->first(); ->first();
if ($du && is_null($du->deleted_at)) { if ($du && is_null($du->deleted_at)) {
abort(409, '이미 배정된 사용자입니다.'); return ApiResponse::error('이미 배정된 사용자입니다.', 409);
} }
if (!empty($p['is_primary']) && (int)$p['is_primary'] === 1) { if (!empty($p['is_primary']) && (int)$p['is_primary'] === 1) {
DepartmentUser::whereNull('deleted_at') DepartmentUser::whereNull('deleted_at')
->where('user_id', $p['user_id']) ->where('user_id', $p['user_id'])
->update(['is_primary'=>0]); ->update(['is_primary' => 0]);
} }
$payload = [ $payload = [
@@ -194,8 +227,13 @@ public static function attachUser(int $deptId, array $params)
DepartmentUser::create($payload); 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) */ /** 사용자 제거(soft) */
@@ -206,10 +244,14 @@ public static function detachUser(int $deptId, int $userId, array $params)
->where('user_id', $userId) ->where('user_id', $userId)
->first(); ->first();
if (!$du) abort(404, '배정된 사용자를 찾을 수 없습니다.'); if (!$du) return ApiResponse::error('배정된 사용자를 찾을 수 없습니다.', 404);
$du->delete(); $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, [ $p = self::v($params, [
'is_primary' => 'required|in:0,1', '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') $du = DepartmentUser::whereNull('deleted_at')
->where('department_id', $deptId) ->where('department_id', $deptId)
->where('user_id', $userId) ->where('user_id', $userId)
->first(); ->first();
if (!$du) abort(404, '배정된 사용자를 찾을 수 없습니다.'); if (!$du) {
return ApiResponse::error('배정된 사용자를 찾을 수 없습니다.', 404);
}
if ((int)$p['is_primary'] === 1) { if ((int)$p['is_primary'] === 1) {
DepartmentUser::whereNull('deleted_at') 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->is_primary = (int)$p['is_primary'];
$du->save(); $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', 'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:1|max:200', 'per_page' => 'nullable|integer|min:1|max:200',
]); ]);
if ($p instanceof JsonResponse) return $p;
$dept = Department::query()->find($deptId); $dept = Department::query()->find($deptId);
if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404);
$q = DepartmentPermission::query() $q = DepartmentPermission::query()
->whereNull('deleted_at') ->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['menu_id'])) $q->where('menu_id', $p['menu_id']);
if (isset($p['is_allowed'])) $q->where('is_allowed', (int)$p['is_allowed']); 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 */ /** 권한 부여/차단 upsert */
@@ -271,9 +326,10 @@ public static function upsertPermission(int $deptId, array $params)
'menu_id' => 'nullable|integer|min:1', 'menu_id' => 'nullable|integer|min:1',
'is_allowed' => 'nullable|in:0,1', 'is_allowed' => 'nullable|in:0,1',
]); ]);
if ($p instanceof JsonResponse) return $p;
$dept = Department::query()->find($deptId); $dept = Department::query()->find($deptId);
if (!$dept) abort(404, '부서를 찾을 수 없습니다.'); if (!$dept) return ApiResponse::error('부서를 찾을 수 없습니다.', 404);
$payload = [ $payload = [
'department_id' => $deptId, 'department_id' => $deptId,
@@ -286,6 +342,7 @@ public static function upsertPermission(int $deptId, array $params)
$model->deleted_at = null; $model->deleted_at = null;
$model->save(); $model->save();
// 변경 후 목록 반환
return self::listPermissions($deptId, []); return self::listPermissions($deptId, []);
} }
@@ -295,6 +352,7 @@ public static function revokePermission(int $deptId, int $permissionId, array $p
$p = self::v($params, [ $p = self::v($params, [
'menu_id' => 'nullable|integer|min:1', 'menu_id' => 'nullable|integer|min:1',
]); ]);
if ($p instanceof JsonResponse) return $p;
$q = DepartmentPermission::whereNull('deleted_at') $q = DepartmentPermission::whereNull('deleted_at')
->where('department_id', $deptId) ->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']); if (isset($p['menu_id'])) $q->where('menu_id', $p['menu_id']);
$rows = $q->get(); $rows = $q->get();
if ($rows->isEmpty()) abort(404, '대상 권한을 찾을 수 없습니다.'); if ($rows->isEmpty()) return ApiResponse::error('대상 권한을 찾을 수 없습니다.', 404);
foreach ($rows as $row) $row->delete(); foreach ($rows as $row) $row->delete();
return [ return ApiResponse::response('result', [
'permission_id' => $permissionId, 'permission_id' => $permissionId,
'menu_id' => $p['menu_id'] ?? null, 'menu_id' => $p['menu_id'] ?? null,
'deleted_count' => $rows->count(), 'deleted_count' => $rows->count(),
]; ]);
} }
} }

View File

@@ -2,67 +2,77 @@
namespace App\Services; namespace App\Services;
use Illuminate\Support\Facades\DB; use App\Models\Commons\Menu;
use App\Helpers\ApiResponse;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
class MenuService 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) public static function index(array $params)
{ {
$tenantId = self::tenantId($params); $tenantId = self::tenantId();
$q = DB::table('menus')->whereNull('deleted_at'); $q = Menu::query()->withShared($tenantId);
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']);
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) public static function show(array $params)
{ {
$id = (int)($params['id'] ?? 0); $id = (int)($params['id'] ?? 0);
$tenantId = self::tenantId($params); $tenantId = self::tenantId();
$q = DB::table('menus')->where('id', $id)->whereNull('deleted_at'); if (!$id) {
if (!is_null($tenantId)) { return ApiResponse::error('id가 필요합니다.', 400);
$q->where(function ($w) use ($tenantId) {
$w->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
});
} }
$row = $q->first(); $q = Menu::withShared($tenantId)->where('id', $id);
throw_if(!$row, ValidationException::withMessages(['id' => 'Menu not found']));
return $row; // 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) public static function store(array $params)
{ {
$tenantId = self::tenantId($params); $tenantId = self::tenantId();
$userId = self::actorId($params); $userId = self::actorId();
$v = Validator::make($params, [ $v = Validator::make($params, [
'parent_id' => ['nullable','integer'], 'parent_id' => ['nullable','integer'],
'name' => ['required','string','max:100'], 'name' => ['required','string','max:100'],
'slug' => ['nullable','string','max:150'],
'url' => ['nullable','string','max:255'], 'url' => ['nullable','string','max:255'],
'is_active' => ['nullable','boolean'], 'is_active' => ['nullable','boolean'],
'sort_order' => ['nullable','integer'], 'sort_order' => ['nullable','integer'],
@@ -71,52 +81,47 @@ public static function store(array $params)
'external_url' => ['nullable','string','max:255'], 'external_url' => ['nullable','string','max:255'],
'icon' => ['nullable','string','max:50'], 'icon' => ['nullable','string','max:50'],
]); ]);
if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422);
}
$data = $v->validated(); $data = $v->validated();
// slug 유니크(테넌트 범위) 체크 $menu = new Menu();
if (!empty($data['slug'])) { $menu->tenant_id = $tenantId;
$exists = DB::table('menus') $menu->parent_id = $data['parent_id'] ?? null;
->whereNull('deleted_at') $menu->name = $data['name'];
->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) $menu->url = $data['url'] ?? null;
->where('slug',$data['slug']) $menu->is_active = (int)($data['is_active'] ?? 1);
->exists(); $menu->sort_order = (int)($data['sort_order'] ?? 0);
if ($exists) { $menu->hidden = (int)($data['hidden'] ?? 0);
throw ValidationException::withMessages(['slug'=>'이미 사용 중인 슬러그입니다.']); $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([ return ApiResponse::response('result', $menu->fresh());
'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];
} }
/**
* 메뉴 수정
*/
public static function update(array $params) public static function update(array $params)
{ {
$id = (int)($params['id'] ?? 0); $id = (int)($params['id'] ?? 0);
$tenantId = self::tenantId($params); $tenantId = self::tenantId();
$userId = self::actorId($params); $userId = self::actorId();
if (!$id) {
return ApiResponse::error('id가 필요합니다.', 400);
}
$v = Validator::make($params, [ $v = Validator::make($params, [
'parent_id' => ['nullable','integer'], 'parent_id' => ['nullable','integer'],
'name' => ['nullable','string','max:100'], 'name' => ['nullable','string','max:100'],
'slug' => ['nullable','string','max:150'],
'url' => ['nullable','string','max:255'], 'url' => ['nullable','string','max:255'],
'is_active' => ['nullable','boolean'], 'is_active' => ['nullable','boolean'],
'sort_order' => ['nullable','integer'], 'sort_order' => ['nullable','integer'],
@@ -125,50 +130,55 @@ public static function update(array $params)
'external_url' => ['nullable','string','max:255'], 'external_url' => ['nullable','string','max:255'],
'icon' => ['nullable','string','max:50'], 'icon' => ['nullable','string','max:50'],
]); ]);
if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422);
}
$data = $v->validated(); $data = $v->validated();
// 대상 존재 확인 & 테넌트 범위 $menu = Menu::withShared($tenantId)->where('id', $id)->first();
$exists = DB::table('menus')->where('id',$id)->whereNull('deleted_at') if (!$menu) {
->when(!is_null($tenantId), fn($q)=>$q->where('tenant_id',$tenantId), fn($q)=>$q->whereNull('tenant_id')) return ApiResponse::error('Menu not found', 404);
->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'=>'이미 사용 중인 슬러그입니다.']);
} }
$update = Arr::only($data, ['parent_id','name','slug','url','is_active','sort_order','hidden','is_external','external_url','icon']); $update = Arr::only($data, [
$update = array_filter($update, fn($v)=>!is_null($v)); 'parent_id','name','url','is_active','sort_order','hidden','is_external','external_url','icon'
$update['updated_at'] = now(); ]);
$update['updated_by'] = $userId; $update = array_filter($update, fn($v) => !is_null($v));
DB::table('menus')->where('id',$id)->update($update); if (empty($update)) {
return ['id' => $id]; return ApiResponse::error('수정할 데이터가 없습니다.', 400);
}
$update['updated_by'] = $userId;
$menu->fill($update)->save();
return ApiResponse::response('result', $menu->fresh());
} }
/**
* 메뉴 삭제(소프트)
*/
public static function destroy(array $params) public static function destroy(array $params)
{ {
$id = (int)($params['id'] ?? 0); $id = (int)($params['id'] ?? 0);
$tenantId = self::tenantId($params); $tenantId = self::tenantId();
$userId = self::actorId($params); $userId = self::actorId();
$q = DB::table('menus')->where('id',$id)->whereNull('deleted_at'); if (!$id) {
$q = !is_null($tenantId) return ApiResponse::error('id가 필요합니다.', 400);
? $q->where('tenant_id',$tenantId) }
: $q->whereNull('tenant_id');
$row = $q->first(); $menu = Menu::withShared($tenantId)->where('id', $id)->first();
if (!$row) throw ValidationException::withMessages(['id'=>'Menu not found']); if (!$menu) {
return ApiResponse::error('Menu not found', 404);
}
DB::table('menus')->where('id',$id)->update([ $menu->deleted_by = $userId;
'deleted_at' => now(), $menu->save();
'deleted_by' => $userId, $menu->delete();
]);
return ['id' => $id, 'deleted' => true]; return ApiResponse::response('success');
} }
/** /**
@@ -178,43 +188,56 @@ public static function destroy(array $params)
public static function reorder(array $params) public static function reorder(array $params)
{ {
if (!is_array($params) || empty($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) { foreach ($params as $it) {
if (!isset($it['id'], $it['sort_order'])) continue; if (!isset($it['id'], $it['sort_order'])) continue;
DB::table('menus')->where('id',(int)$it['id'])->update([
'sort_order' => (int)$it['sort_order'], $menu = Menu::withShared($tenantId)->find((int)$it['id']);
'updated_at' => now(), 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) public static function toggle(array $params)
{ {
$id = (int)($params['id'] ?? 0); $id = (int)($params['id'] ?? 0);
$userId = self::actorId($params); $tenantId = self::tenantId();
$userId = self::actorId();
$payload = array_filter([ if (!$id) {
'is_active' => isset($params['is_active']) ? (int)$params['is_active'] : null, return ApiResponse::error('id가 필요합니다.', 400);
'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'=>'변경할 필드가 없습니다.']);
} }
$payload['updated_at'] = now(); $payload = array_filter([
$payload['updated_by'] = $userId; '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); if (empty($payload)) {
return ['id' => $id]; 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());
} }
} }