feat: [rd] 조직도 관리 화면 추가

- SortableJS 기반 drag & drop 부서 배치 UI
- 미배치 직원 패널 + 부서 트리 (3단계 계층 지원)
- 직원 배치/해제 API 엔드포인트
- 실시간 저장 및 인원수 표시
This commit is contained in:
김보곤
2026-03-06 19:34:52 +09:00
parent 08bf255480
commit 3fccd7414c
3 changed files with 469 additions and 1 deletions

View File

@@ -2,8 +2,11 @@
namespace App\Http\Controllers;
use App\Models\HR\Employee;
use App\Models\Rd\AiQuotation;
use App\Models\Tenants\Department;
use App\Services\Rd\AiQuotationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -28,6 +31,114 @@ public function index(Request $request): View|\Illuminate\Http\Response
return view('rd.index', compact('dashboard', 'statuses'));
}
/**
* 조직도 관리
*/
public function orgChart(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.org-chart'));
}
$tenantId = session('selected_tenant_id');
// 부서 트리 (parent_id=null이 최상위)
$departments = Department::where('tenant_id', $tenantId)
->where('is_active', true)
->orderBy('sort_order')
->orderBy('name')
->get();
// 전체 직원 (활성 상태)
$employees = Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('employee_status', 'active')
->with(['user', 'department'])
->orderBy('display_name')
->get();
// 미배치 직원 (department_id가 null)
$unassigned = $employees->whereNull('department_id');
// 배치된 직원을 부서별로 그룹핑
$assignedByDept = $employees->whereNotNull('department_id')->groupBy('department_id');
return view('rd.org-chart', compact('departments', 'employees', 'unassigned', 'assignedByDept'));
}
/**
* 조직도 - 직원 부서 배치
*/
public function orgChartAssign(Request $request): JsonResponse
{
$request->validate([
'employee_id' => 'required|integer',
'department_id' => 'required|integer',
]);
$tenantId = session('selected_tenant_id');
$employee = Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('id', $request->employee_id)
->first();
if (! $employee) {
return response()->json(['success' => false, 'message' => '직원을 찾을 수 없습니다.'], 404);
}
$employee->department_id = $request->department_id;
$employee->save();
return response()->json(['success' => true]);
}
/**
* 조직도 - 직원 부서 해제 (미배치로 이동)
*/
public function orgChartUnassign(Request $request): JsonResponse
{
$request->validate([
'employee_id' => 'required|integer',
]);
$tenantId = session('selected_tenant_id');
$employee = Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('id', $request->employee_id)
->first();
if (! $employee) {
return response()->json(['success' => false, 'message' => '직원을 찾을 수 없습니다.'], 404);
}
$employee->department_id = null;
$employee->save();
return response()->json(['success' => true]);
}
/**
* 조직도 - 부서 내 직원 순서/이동 일괄 처리
*/
public function orgChartReorder(Request $request): JsonResponse
{
$request->validate([
'moves' => 'required|array',
'moves.*.employee_id' => 'required|integer',
'moves.*.department_id' => 'nullable|integer',
]);
$tenantId = session('selected_tenant_id');
foreach ($request->moves as $move) {
Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('id', $move['employee_id'])
->update(['department_id' => $move['department_id']]);
}
return response()->json(['success' => true]);
}
/**
* 중대재해처벌법 실무 점검
*/