2025-08-16 03:25:06 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
2025-08-21 18:38:27 +09:00
|
|
|
use App\Models\Tenants\Department;
|
|
|
|
|
use App\Models\Tenants\Pivots\DepartmentUser;
|
2025-08-16 04:16:34 +09:00
|
|
|
use Carbon\Carbon;
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
2025-08-16 03:25:06 +09:00
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
|
|
2025-08-20 20:23:01 +09:00
|
|
|
class DepartmentService extends Service
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-16 04:16:34 +09:00
|
|
|
/**
|
2025-08-20 20:23:01 +09:00
|
|
|
* 공통 검증 헬퍼: 실패 시 ['error'=>..., 'code'=>...] 형태로 반환
|
2025-08-16 04:16:34 +09:00
|
|
|
*/
|
2025-08-20 20:23:01 +09:00
|
|
|
protected function v(array $params, array $rules)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
|
|
|
|
$v = Validator::make($params, $rules);
|
2025-08-16 04:16:34 +09:00
|
|
|
if ($v->fails()) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => $v->errors()->first(), 'code' => 422];
|
2025-08-16 04:16:34 +09:00
|
|
|
}
|
2025-11-06 17:45:49 +09:00
|
|
|
|
2025-08-16 03:25:06 +09:00
|
|
|
return $v->validated();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 목록 */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function index(array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-20 20:23:01 +09:00
|
|
|
$p = $this->v($params, [
|
2025-11-06 17:45:49 +09:00
|
|
|
'q' => 'nullable|string|max:100',
|
2025-08-16 03:25:06 +09:00
|
|
|
'is_active' => 'nullable|in:0,1',
|
2025-11-06 17:45:49 +09:00
|
|
|
'page' => 'nullable|integer|min:1',
|
|
|
|
|
'per_page' => 'nullable|integer|min:1|max:200',
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
$q = Department::query();
|
|
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
if (isset($p['is_active'])) {
|
2025-11-06 17:45:49 +09:00
|
|
|
$q->where('is_active', (int) $p['is_active']);
|
2025-08-16 04:16:34 +09:00
|
|
|
}
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! empty($p['q'])) {
|
2025-08-16 04:16:34 +09:00
|
|
|
$q->where(function ($w) use ($p) {
|
2025-11-06 17:45:49 +09:00
|
|
|
$w->where('name', 'like', '%'.$p['q'].'%')
|
|
|
|
|
->orWhere('code', 'like', '%'.$p['q'].'%');
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
$q->orderBy('sort_order')->orderBy('name');
|
|
|
|
|
|
|
|
|
|
$perPage = $p['per_page'] ?? 20;
|
2025-11-06 17:45:49 +09:00
|
|
|
$page = $p['page'] ?? null;
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $q->paginate($perPage, ['*'], 'page', $page);
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-09 20:27:44 +09:00
|
|
|
/** 부서 트리 조회 */
|
|
|
|
|
public function tree(array $params = []): array
|
|
|
|
|
{
|
|
|
|
|
$p = $this->v($params, [
|
|
|
|
|
'with_users' => 'nullable|in:0,1,true,false',
|
|
|
|
|
]);
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$withUsers = filter_var($p['with_users'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
|
|
|
|
|
|
|
|
// 최상위 부서 조회 (parent_id가 null인 부서)
|
|
|
|
|
$query = Department::query()
|
|
|
|
|
->whereNull('parent_id')
|
|
|
|
|
->orderBy('sort_order')
|
|
|
|
|
->orderBy('name');
|
|
|
|
|
|
|
|
|
|
// 재귀적으로 자식 부서 로드
|
|
|
|
|
$query->with(['children' => function ($q) use ($withUsers) {
|
|
|
|
|
$q->orderBy('sort_order')->orderBy('name');
|
|
|
|
|
$this->loadChildrenRecursive($q, $withUsers);
|
|
|
|
|
}]);
|
|
|
|
|
|
2025-12-26 15:00:57 +09:00
|
|
|
// employees 관계 사용 (tenant_user_profiles 기반)
|
2025-12-09 20:27:44 +09:00
|
|
|
if ($withUsers) {
|
2025-12-26 15:00:57 +09:00
|
|
|
$query->with(['employees.user:id,name,email']);
|
2025-12-09 20:27:44 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 15:00:57 +09:00
|
|
|
$result = $query->get();
|
|
|
|
|
|
|
|
|
|
// users 형태로 변환 (프론트엔드 호환성)
|
|
|
|
|
if ($withUsers) {
|
|
|
|
|
$result = $this->transformEmployeesToUsers($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result->toArray();
|
2025-12-09 20:27:44 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 재귀적으로 자식 부서 로드 */
|
|
|
|
|
private function loadChildrenRecursive($query, bool $withUsers): void
|
|
|
|
|
{
|
|
|
|
|
$query->with(['children' => function ($q) use ($withUsers) {
|
|
|
|
|
$q->orderBy('sort_order')->orderBy('name');
|
|
|
|
|
$this->loadChildrenRecursive($q, $withUsers);
|
|
|
|
|
}]);
|
|
|
|
|
|
2025-12-26 15:00:57 +09:00
|
|
|
// employees 관계 사용 (tenant_user_profiles 기반)
|
2025-12-09 20:27:44 +09:00
|
|
|
if ($withUsers) {
|
2025-12-26 15:00:57 +09:00
|
|
|
$query->with(['employees.user:id,name,email']);
|
2025-12-09 20:27:44 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 15:00:57 +09:00
|
|
|
/** employees를 users 형태로 변환 (재귀) */
|
|
|
|
|
private function transformEmployeesToUsers($departments)
|
|
|
|
|
{
|
|
|
|
|
return $departments->map(function ($dept) {
|
|
|
|
|
// employees → users 변환
|
|
|
|
|
if ($dept->relationLoaded('employees')) {
|
|
|
|
|
$users = $dept->employees->map(function ($profile) {
|
|
|
|
|
return $profile->user;
|
|
|
|
|
})->filter(); // null 제거
|
|
|
|
|
|
|
|
|
|
$dept->setRelation('users', $users);
|
|
|
|
|
$dept->unsetRelation('employees');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 자식 부서도 재귀 처리
|
|
|
|
|
if ($dept->relationLoaded('children') && $dept->children->count() > 0) {
|
|
|
|
|
$dept->setRelation('children', $this->transformEmployeesToUsers($dept->children));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $dept;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 03:25:06 +09:00
|
|
|
/** 생성 */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function store(array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-21 15:43:06 +09:00
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$userId = $this->apiUserId();
|
2025-08-20 20:23:01 +09:00
|
|
|
|
|
|
|
|
$p = $this->v($params, [
|
2025-12-25 03:48:32 +09:00
|
|
|
'parent_id' => 'nullable|integer|min:1',
|
2025-11-06 17:45:49 +09:00
|
|
|
'code' => 'nullable|string|max:50',
|
|
|
|
|
'name' => 'required|string|max:100',
|
2025-08-16 03:25:06 +09:00
|
|
|
'description' => 'nullable|string|max:255',
|
2025-11-06 17:45:49 +09:00
|
|
|
'is_active' => 'nullable|in:0,1',
|
|
|
|
|
'sort_order' => 'nullable|integer',
|
|
|
|
|
'created_by' => 'nullable|integer|min:1',
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-12-25 03:48:32 +09:00
|
|
|
// parent_id 유효성 검사
|
|
|
|
|
if (! empty($p['parent_id'])) {
|
|
|
|
|
$parent = Department::query()->find($p['parent_id']);
|
|
|
|
|
if (! $parent) {
|
|
|
|
|
return ['error' => '상위 부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! empty($p['code'])) {
|
2025-08-16 03:25:06 +09:00
|
|
|
$exists = Department::query()->where('code', $p['code'])->exists();
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($exists) {
|
|
|
|
|
return ['error' => '이미 존재하는 부서 코드입니다.', 'code' => 409];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dept = Department::create([
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
2025-12-25 03:48:32 +09:00
|
|
|
'parent_id' => $p['parent_id'] ?? null,
|
2025-11-06 17:45:49 +09:00
|
|
|
'code' => $p['code'] ?? null,
|
|
|
|
|
'name' => $p['name'],
|
2025-08-16 03:25:06 +09:00
|
|
|
'description' => $p['description'] ?? null,
|
2025-11-06 17:45:49 +09:00
|
|
|
'is_active' => isset($p['is_active']) ? (int) $p['is_active'] : 1,
|
|
|
|
|
'sort_order' => $p['sort_order'] ?? 0,
|
|
|
|
|
'created_by' => $userId ?? null,
|
|
|
|
|
'updated_by' => $userId ?? null,
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $dept->fresh();
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 단건 */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function show(int $id, array $params = [])
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $id) {
|
|
|
|
|
return ['error' => 'id가 필요합니다.', 'code' => 400];
|
|
|
|
|
}
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-20 20:23:01 +09:00
|
|
|
$res = Department::query()->find($id);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $res) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
2025-08-16 04:16:34 +09:00
|
|
|
}
|
2025-11-06 17:45:49 +09:00
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
return $res;
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 수정 */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function update(int $id, array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $id) {
|
|
|
|
|
return ['error' => 'id가 필요합니다.', 'code' => 400];
|
|
|
|
|
}
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-20 20:23:01 +09:00
|
|
|
$p = $this->v($params, [
|
2025-12-25 03:48:32 +09:00
|
|
|
'parent_id' => 'nullable|integer|min:0', // 0이면 최상위로 이동
|
2025-11-06 17:45:49 +09:00
|
|
|
'code' => 'nullable|string|max:50',
|
|
|
|
|
'name' => 'nullable|string|max:100',
|
2025-08-16 03:25:06 +09:00
|
|
|
'description' => 'nullable|string|max:255',
|
2025-11-06 17:45:49 +09:00
|
|
|
'is_active' => 'nullable|in:0,1',
|
|
|
|
|
'sort_order' => 'nullable|integer',
|
|
|
|
|
'updated_by' => 'nullable|integer|min:1',
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
$dept = Department::query()->find($id);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $dept) {
|
|
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-12-25 03:48:32 +09:00
|
|
|
// parent_id 유효성 검사
|
|
|
|
|
if (array_key_exists('parent_id', $p)) {
|
|
|
|
|
$parentId = $p['parent_id'];
|
|
|
|
|
if ($parentId === 0) {
|
|
|
|
|
$parentId = null; // 0이면 최상위로 이동
|
|
|
|
|
} elseif ($parentId) {
|
|
|
|
|
if ($parentId === $id) {
|
|
|
|
|
return ['error' => '자기 자신을 상위 부서로 설정할 수 없습니다.', 'code' => 422];
|
|
|
|
|
}
|
|
|
|
|
$parent = Department::query()->find($parentId);
|
|
|
|
|
if (! $parent) {
|
|
|
|
|
return ['error' => '상위 부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if (array_key_exists('code', $p) && ! is_null($p['code'])) {
|
2025-08-16 03:25:06 +09:00
|
|
|
$exists = Department::query()
|
|
|
|
|
->where('code', $p['code'])
|
2025-08-16 04:16:34 +09:00
|
|
|
->where('id', '!=', $id)
|
2025-08-16 03:25:06 +09:00
|
|
|
->exists();
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($exists) {
|
|
|
|
|
return ['error' => '이미 존재하는 부서 코드입니다.', 'code' => 409];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-25 03:48:32 +09:00
|
|
|
$fillData = [
|
2025-11-06 17:45:49 +09:00
|
|
|
'code' => array_key_exists('code', $p) ? $p['code'] : $dept->code,
|
|
|
|
|
'name' => $p['name'] ?? $dept->name,
|
2025-08-16 03:25:06 +09:00
|
|
|
'description' => $p['description'] ?? $dept->description,
|
2025-11-06 17:45:49 +09:00
|
|
|
'is_active' => isset($p['is_active']) ? (int) $p['is_active'] : $dept->is_active,
|
|
|
|
|
'sort_order' => $p['sort_order'] ?? $dept->sort_order,
|
|
|
|
|
'updated_by' => $p['updated_by'] ?? $dept->updated_by,
|
2025-12-25 03:48:32 +09:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// parent_id 업데이트
|
|
|
|
|
if (array_key_exists('parent_id', $p)) {
|
|
|
|
|
$fillData['parent_id'] = $p['parent_id'] === 0 ? null : $p['parent_id'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dept->fill($fillData)->save();
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $dept->fresh();
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 삭제(soft) */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function destroy(int $id, array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $id) {
|
|
|
|
|
return ['error' => 'id가 필요합니다.', 'code' => 400];
|
|
|
|
|
}
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-20 20:23:01 +09:00
|
|
|
$p = $this->v($params, [
|
2025-08-16 03:25:06 +09:00
|
|
|
'deleted_by' => 'nullable|integer|min:1',
|
|
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
$dept = Department::query()->find($id);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $dept) {
|
|
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! empty($p['deleted_by'])) {
|
2025-08-16 03:25:06 +09:00
|
|
|
$dept->deleted_by = $p['deleted_by'];
|
|
|
|
|
$dept->save();
|
|
|
|
|
}
|
|
|
|
|
$dept->delete();
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['id' => $id, 'deleted_at' => now()->toDateTimeString()];
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 부서 사용자 목록 */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function listUsers(int $deptId, array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-20 20:23:01 +09:00
|
|
|
$p = $this->v($params, [
|
2025-11-06 17:45:49 +09:00
|
|
|
'page' => 'nullable|integer|min:1',
|
|
|
|
|
'per_page' => 'nullable|integer|min:1|max:200',
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
$dept = Department::query()->find($deptId);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $dept) {
|
|
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
$builder = $dept->departmentUsers()->with('user')
|
|
|
|
|
->orderByDesc('is_primary')->orderBy('id');
|
|
|
|
|
|
|
|
|
|
$perPage = $p['per_page'] ?? 20;
|
2025-11-06 17:45:49 +09:00
|
|
|
$page = $p['page'] ?? null;
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $builder->paginate($perPage, ['*'], 'page', $page);
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 사용자 배정 (단건) */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function attachUser(int $deptId, array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-20 20:23:01 +09:00
|
|
|
$p = $this->v($params, [
|
2025-11-06 17:45:49 +09:00
|
|
|
'user_id' => 'required|integer|min:1',
|
2025-08-16 03:25:06 +09:00
|
|
|
'is_primary' => 'nullable|in:0,1',
|
2025-11-06 17:45:49 +09:00
|
|
|
'joined_at' => 'nullable|date',
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
$dept = Department::query()->find($deptId);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $dept) {
|
|
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
$result = DB::transaction(function () use ($dept, $p) {
|
2025-08-21 15:43:06 +09:00
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
2025-08-16 03:25:06 +09:00
|
|
|
$du = DepartmentUser::withTrashed()
|
|
|
|
|
->where('department_id', $dept->id)
|
|
|
|
|
->where('user_id', $p['user_id'])
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($du && is_null($du->deleted_at)) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '이미 배정된 사용자입니다.', 'code' => 409];
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! empty($p['is_primary']) && (int) $p['is_primary'] === 1) {
|
2025-08-16 03:25:06 +09:00
|
|
|
DepartmentUser::whereNull('deleted_at')
|
|
|
|
|
->where('user_id', $p['user_id'])
|
2025-08-16 04:16:34 +09:00
|
|
|
->update(['is_primary' => 0]);
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$payload = [
|
|
|
|
|
'department_id' => $dept->id,
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'user_id' => $p['user_id'],
|
|
|
|
|
'is_primary' => isset($p['is_primary']) ? (int) $p['is_primary'] : 0,
|
|
|
|
|
'joined_at' => ! empty($p['joined_at']) ? Carbon::parse($p['joined_at']) : now(),
|
2025-08-16 03:25:06 +09:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($du) {
|
|
|
|
|
$du->fill($payload);
|
|
|
|
|
$du->restore();
|
|
|
|
|
$du->save();
|
|
|
|
|
} else {
|
|
|
|
|
DepartmentUser::create($payload);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
return ['department_id' => $dept->id, 'user_id' => $p['user_id']];
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($result instanceof JsonResponse) {
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $result;
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 사용자 제거(soft) */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function detachUser(int $deptId, int $userId, array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
|
|
|
|
$du = DepartmentUser::whereNull('deleted_at')
|
|
|
|
|
->where('department_id', $deptId)
|
|
|
|
|
->where('user_id', $userId)
|
|
|
|
|
->first();
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $du) {
|
|
|
|
|
return ['error' => '배정된 사용자를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-16 03:25:06 +09:00
|
|
|
$du->delete();
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return [
|
2025-11-06 17:45:49 +09:00
|
|
|
'user_id' => $userId,
|
2025-08-16 04:16:34 +09:00
|
|
|
'deleted_at' => now()->toDateTimeString(),
|
2025-08-19 12:41:17 +09:00
|
|
|
];
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 주부서 설정/해제 */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function setPrimary(int $deptId, int $userId, array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-20 20:23:01 +09:00
|
|
|
$p = $this->v($params, [
|
2025-08-16 03:25:06 +09:00
|
|
|
'is_primary' => 'required|in:0,1',
|
|
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
$result = DB::transaction(function () use ($deptId, $userId, $p) {
|
2025-08-16 03:25:06 +09:00
|
|
|
$du = DepartmentUser::whereNull('deleted_at')
|
|
|
|
|
->where('department_id', $deptId)
|
|
|
|
|
->where('user_id', $userId)
|
|
|
|
|
->first();
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $du) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '배정된 사용자를 찾을 수 없습니다.', 'code' => 404];
|
2025-08-16 04:16:34 +09:00
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if ((int) $p['is_primary'] === 1) {
|
2025-08-16 03:25:06 +09:00
|
|
|
DepartmentUser::whereNull('deleted_at')
|
|
|
|
|
->where('user_id', $userId)
|
|
|
|
|
->update(['is_primary' => 0]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$du->is_primary = (int) $p['is_primary'];
|
2025-08-16 03:25:06 +09:00
|
|
|
$du->save();
|
|
|
|
|
|
2025-08-16 04:16:34 +09:00
|
|
|
return ['user_id' => $userId, 'department_id' => $deptId, 'is_primary' => $du->is_primary];
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($result instanceof JsonResponse) {
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $result;
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 부서 권한 목록 */
|
2025-08-20 20:23:01 +09:00
|
|
|
public function listPermissions(int $deptId, array $params)
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-21 18:38:27 +09:00
|
|
|
// 1) 파라미터 검증
|
2025-08-20 20:23:01 +09:00
|
|
|
$p = $this->v($params, [
|
2025-11-06 17:45:49 +09:00
|
|
|
'menu_id' => 'nullable|integer|min:1',
|
|
|
|
|
'is_allowed' => 'nullable|in:0,1',
|
|
|
|
|
'page' => 'nullable|integer|min:1',
|
|
|
|
|
'per_page' => 'nullable|integer|min:1|max:200',
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($p instanceof JsonResponse) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
|
|
|
|
if (isset($p['error'])) {
|
|
|
|
|
return $p;
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-21 18:38:27 +09:00
|
|
|
// 2) 부서 확인
|
2025-08-16 03:25:06 +09:00
|
|
|
$dept = Department::query()->find($deptId);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $dept) {
|
|
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$tenantId = (int) $dept->tenant_id;
|
2025-08-21 18:38:27 +09:00
|
|
|
$modelType = Department::class;
|
|
|
|
|
|
|
|
|
|
// 3) ALLOW/DENY 서브쿼리 준비 (컬럼 형태 통일)
|
|
|
|
|
$allowSub = DB::table('model_has_permissions')
|
|
|
|
|
->select([
|
|
|
|
|
'permission_id',
|
|
|
|
|
DB::raw('1 as is_allowed'),
|
|
|
|
|
DB::raw('NULL as reason'),
|
|
|
|
|
DB::raw('NULL as effective_from'),
|
|
|
|
|
DB::raw('NULL as effective_to'),
|
|
|
|
|
DB::raw('NULL as override_id'),
|
|
|
|
|
DB::raw('NULL as override_updated_at'),
|
|
|
|
|
])
|
|
|
|
|
->where([
|
|
|
|
|
'model_type' => $modelType,
|
2025-11-06 17:45:49 +09:00
|
|
|
'model_id' => $deptId,
|
|
|
|
|
'tenant_id' => $tenantId,
|
2025-08-21 18:38:27 +09:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$denySub = DB::table('permission_overrides')
|
|
|
|
|
->select([
|
|
|
|
|
'permission_id',
|
|
|
|
|
DB::raw('0 as is_allowed'),
|
|
|
|
|
'reason',
|
|
|
|
|
'effective_from',
|
|
|
|
|
'effective_to',
|
|
|
|
|
'id as override_id',
|
|
|
|
|
'updated_at as override_updated_at',
|
|
|
|
|
])
|
|
|
|
|
->where([
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
2025-08-21 18:38:27 +09:00
|
|
|
'model_type' => $modelType,
|
2025-11-06 17:45:49 +09:00
|
|
|
'model_id' => $deptId,
|
2025-08-21 18:38:27 +09:00
|
|
|
])
|
|
|
|
|
->where('effect', -1);
|
|
|
|
|
|
|
|
|
|
// 4) 합치고 permissions 조인
|
|
|
|
|
$q = DB::query()
|
|
|
|
|
->fromSub(function ($sub) use ($allowSub, $denySub) {
|
|
|
|
|
$sub->from($allowSub->unionAll($denySub), 'u');
|
|
|
|
|
}, 'u')
|
|
|
|
|
->join('permissions', 'permissions.id', '=', 'u.permission_id')
|
|
|
|
|
->select([
|
|
|
|
|
'u.permission_id',
|
|
|
|
|
'u.is_allowed',
|
|
|
|
|
'permissions.name as permission_code',
|
|
|
|
|
'permissions.guard_name',
|
|
|
|
|
'u.reason',
|
|
|
|
|
'u.effective_from',
|
|
|
|
|
'u.effective_to',
|
|
|
|
|
'u.override_id',
|
|
|
|
|
'u.override_updated_at',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 5) 필터
|
|
|
|
|
if (isset($p['is_allowed'])) {
|
2025-11-06 17:45:49 +09:00
|
|
|
$q->where('u.is_allowed', (int) $p['is_allowed']);
|
2025-08-21 18:38:27 +09:00
|
|
|
}
|
|
|
|
|
if (isset($p['menu_id'])) {
|
|
|
|
|
// 권한코드가 menu.{id}.xxx 형태라는 전제
|
2025-11-06 17:45:49 +09:00
|
|
|
$q->where('permissions.name', 'like', 'menu.'.(int) $p['menu_id'].'.%');
|
2025-08-21 18:38:27 +09:00
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-21 18:38:27 +09:00
|
|
|
// 6) 정렬(ALLOW 우선, 그 다음 permission_id 오름차순)
|
|
|
|
|
$q->orderByDesc('u.is_allowed')->orderBy('u.permission_id');
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-21 18:38:27 +09:00
|
|
|
// 7) 페이지네이션
|
2025-08-16 04:16:34 +09:00
|
|
|
$perPage = $p['per_page'] ?? 20;
|
2025-11-06 17:45:49 +09:00
|
|
|
$page = $p['page'] ?? null;
|
2025-08-16 04:16:34 +09:00
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $q->paginate($perPage, ['*'], 'page', $page);
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 권한 부여/차단 upsert */
|
2025-08-21 18:38:27 +09:00
|
|
|
public function upsertPermissions(int $deptId, array $payload): array
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-21 18:38:27 +09:00
|
|
|
// 단일이면 items로 감싸기
|
|
|
|
|
$items = isset($payload['permission_id'])
|
2025-11-06 17:45:49 +09:00
|
|
|
? [$payload]
|
2025-08-21 18:38:27 +09:00
|
|
|
: ($payload['items'] ?? $payload);
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$v = Validator::make(['items' => $items], [
|
2025-08-21 18:38:27 +09:00
|
|
|
'items' => 'required|array|max:1000',
|
|
|
|
|
'items.*.permission_id' => 'required|integer|min:1',
|
2025-11-06 17:45:49 +09:00
|
|
|
'items.*.is_allowed' => 'nullable|in:0,1', // 생략 시 1(허용)
|
2025-08-16 03:25:06 +09:00
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($v->fails()) {
|
|
|
|
|
return ['error' => $v->errors()->first(), 'code' => 422];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
$dept = Department::query()->find($deptId);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $dept) {
|
|
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$tenantId = (int) $dept->tenant_id;
|
2025-08-21 18:38:27 +09:00
|
|
|
$modelType = Department::class;
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$ok = 0;
|
|
|
|
|
$failed = [];
|
2025-08-21 18:38:27 +09:00
|
|
|
|
|
|
|
|
DB::transaction(function () use ($items, $tenantId, $deptId, $modelType, &$ok, &$failed) {
|
|
|
|
|
foreach ($items as $i => $r) {
|
|
|
|
|
try {
|
2025-11-06 17:45:49 +09:00
|
|
|
$permissionId = (int) $r['permission_id'];
|
|
|
|
|
$isAllowed = array_key_exists('is_allowed', $r) ? (int) $r['is_allowed'] : 1;
|
2025-08-21 18:38:27 +09:00
|
|
|
|
|
|
|
|
if ($isAllowed === 1) {
|
|
|
|
|
// ALLOW: Spatie 표준에 넣고, 동일 권한의 DENY 제거
|
|
|
|
|
$exists = DB::table('model_has_permissions')->where([
|
|
|
|
|
'permission_id' => $permissionId,
|
2025-11-06 17:45:49 +09:00
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
|
|
|
|
'tenant_id' => $tenantId,
|
2025-08-21 18:38:27 +09:00
|
|
|
])->exists();
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $exists) {
|
2025-08-21 18:38:27 +09:00
|
|
|
DB::table('model_has_permissions')->insert([
|
|
|
|
|
'permission_id' => $permissionId,
|
2025-11-06 17:45:49 +09:00
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
|
|
|
|
'tenant_id' => $tenantId,
|
2025-08-21 18:38:27 +09:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::table('permission_overrides')->where([
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
2025-08-21 18:38:27 +09:00
|
|
|
'permission_id' => $permissionId,
|
2025-11-06 17:45:49 +09:00
|
|
|
'effect' => -1,
|
2025-08-21 18:38:27 +09:00
|
|
|
])->delete();
|
|
|
|
|
} else {
|
|
|
|
|
// DENY: overrides(effect=-1) upsert, 그리고 ALLOW 제거
|
|
|
|
|
$exists = DB::table('permission_overrides')->where([
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
2025-08-21 18:38:27 +09:00
|
|
|
'permission_id' => $permissionId,
|
|
|
|
|
])->exists();
|
|
|
|
|
|
|
|
|
|
if ($exists) {
|
|
|
|
|
DB::table('permission_overrides')->where([
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
2025-08-21 18:38:27 +09:00
|
|
|
'permission_id' => $permissionId,
|
|
|
|
|
])->update(['effect' => -1, 'updated_at' => now()]);
|
|
|
|
|
} else {
|
|
|
|
|
DB::table('permission_overrides')->insert([
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
2025-08-21 18:38:27 +09:00
|
|
|
'permission_id' => $permissionId,
|
2025-11-06 17:45:49 +09:00
|
|
|
'effect' => -1,
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
2025-08-21 18:38:27 +09:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::table('model_has_permissions')->where([
|
|
|
|
|
'permission_id' => $permissionId,
|
2025-11-06 17:45:49 +09:00
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
|
|
|
|
'tenant_id' => $tenantId,
|
2025-08-21 18:38:27 +09:00
|
|
|
])->delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ok++;
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$failed[] = [
|
|
|
|
|
'index' => $i,
|
|
|
|
|
'permission_id' => $r['permission_id'] ?? null,
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-21 18:38:27 +09:00
|
|
|
return [
|
2025-11-06 17:45:49 +09:00
|
|
|
'processed' => count($items),
|
|
|
|
|
'succeeded' => $ok,
|
|
|
|
|
'failed' => $failed,
|
2025-08-21 18:38:27 +09:00
|
|
|
];
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 권한 제거 (menu_id 없으면 전체 제거) */
|
2025-08-21 18:38:27 +09:00
|
|
|
public function revokePermissions(int $deptId, array $payload): array
|
2025-08-16 03:25:06 +09:00
|
|
|
{
|
2025-08-21 18:38:27 +09:00
|
|
|
$items = isset($payload['permission_id'])
|
2025-11-06 17:45:49 +09:00
|
|
|
? [$payload]
|
2025-08-21 18:38:27 +09:00
|
|
|
: ($payload['items'] ?? $payload);
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$v = Validator::make(['items' => $items], [
|
2025-08-21 18:38:27 +09:00
|
|
|
'items' => 'required|array|max:1000',
|
|
|
|
|
'items.*.permission_id' => 'required|integer|min:1',
|
|
|
|
|
]);
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($v->fails()) {
|
|
|
|
|
return ['error' => $v->errors()->first(), 'code' => 422];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-21 18:38:27 +09:00
|
|
|
$dept = Department::query()->find($deptId);
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! $dept) {
|
|
|
|
|
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$tenantId = (int) $dept->tenant_id;
|
2025-08-21 18:38:27 +09:00
|
|
|
$modelType = Department::class;
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
$ok = 0;
|
|
|
|
|
$failed = [];
|
2025-08-21 18:38:27 +09:00
|
|
|
|
|
|
|
|
DB::transaction(function () use ($items, $tenantId, $deptId, $modelType, &$ok, &$failed) {
|
|
|
|
|
foreach ($items as $i => $r) {
|
|
|
|
|
try {
|
2025-11-06 17:45:49 +09:00
|
|
|
$permissionId = (int) $r['permission_id'];
|
2025-08-21 18:38:27 +09:00
|
|
|
|
|
|
|
|
// ALLOW 제거
|
|
|
|
|
DB::table('model_has_permissions')->where([
|
|
|
|
|
'permission_id' => $permissionId,
|
2025-11-06 17:45:49 +09:00
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
|
|
|
|
'tenant_id' => $tenantId,
|
2025-08-21 18:38:27 +09:00
|
|
|
])->delete();
|
|
|
|
|
|
|
|
|
|
// DENY/임시허용 오버라이드 제거
|
|
|
|
|
DB::table('permission_overrides')->where([
|
2025-11-06 17:45:49 +09:00
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'model_type' => $modelType,
|
|
|
|
|
'model_id' => $deptId,
|
2025-08-21 18:38:27 +09:00
|
|
|
'permission_id' => $permissionId,
|
|
|
|
|
])->delete();
|
|
|
|
|
|
|
|
|
|
$ok++;
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$failed[] = [
|
|
|
|
|
'index' => $i,
|
|
|
|
|
'permission_id' => $r['permission_id'] ?? null,
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-16 03:25:06 +09:00
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return [
|
2025-08-21 18:38:27 +09:00
|
|
|
'processed' => count($items),
|
|
|
|
|
'succeeded' => $ok,
|
2025-11-06 17:45:49 +09:00
|
|
|
'failed' => $failed,
|
2025-08-19 12:41:17 +09:00
|
|
|
];
|
2025-08-16 03:25:06 +09:00
|
|
|
}
|
|
|
|
|
}
|