diff --git a/app/Http/Controllers/Api/Admin/DepartmentController.php b/app/Http/Controllers/Api/Admin/DepartmentController.php new file mode 100644 index 00000000..b5432b5c --- /dev/null +++ b/app/Http/Controllers/Api/Admin/DepartmentController.php @@ -0,0 +1,157 @@ +departmentService->getDepartments( + $request->all(), + $request->integer('per_page', 15) + ); + + // HTMX 요청 시 HTML 반환 + if ($request->header('HX-Request')) { + $html = view('departments.partials.table', compact('departments'))->render(); + + return response()->json([ + 'html' => $html, + ]); + } + + // 일반 요청 시 JSON 반환 + return response()->json([ + 'success' => true, + 'data' => $departments->items(), + 'meta' => [ + 'current_page' => $departments->currentPage(), + 'last_page' => $departments->lastPage(), + 'per_page' => $departments->perPage(), + 'total' => $departments->total(), + ], + ]); + } + + /** + * 부서 생성 + */ + public function store(StoreDepartmentRequest $request): JsonResponse + { + $department = $this->departmentService->createDepartment($request->validated()); + + // HTMX 요청 시 성공 메시지와 리다이렉트 헤더 반환 + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '부서가 생성되었습니다.', + 'redirect' => route('departments.index'), + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '부서가 생성되었습니다.', + 'data' => $department, + ], 201); + } + + /** + * 특정 부서 조회 + */ + public function show(Request $request, int $id): JsonResponse + { + $department = $this->departmentService->getDepartmentById($id); + + if (!$department) { + return response()->json([ + 'success' => false, + 'message' => '부서를 찾을 수 없습니다.', + ], 404); + } + + // HTMX 요청 시 HTML 반환 + if ($request->header('HX-Request')) { + return response()->json([ + 'html' => view('departments.partials.detail', compact('department'))->render(), + ]); + } + + return response()->json([ + 'success' => true, + 'data' => $department, + ]); + } + + /** + * 부서 수정 + */ + public function update(UpdateDepartmentRequest $request, int $id): JsonResponse + { + $result = $this->departmentService->updateDepartment($id, $request->validated()); + + if (!$result) { + return response()->json([ + 'success' => false, + 'message' => '부서 수정에 실패했습니다.', + ], 400); + } + + // HTMX 요청 시 성공 메시지와 리다이렉트 헤더 반환 + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '부서가 수정되었습니다.', + 'redirect' => route('departments.index'), + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '부서가 수정되었습니다.', + ]); + } + + /** + * 부서 삭제 (Soft Delete) + */ + public function destroy(Request $request, int $id): JsonResponse + { + $result = $this->departmentService->deleteDepartment($id); + + if (!$result) { + return response()->json([ + 'success' => false, + 'message' => '부서 삭제에 실패했습니다. (하위 부서가 존재할 수 있습니다)', + ], 400); + } + + // HTMX 요청 시 테이블 행 제거 트리거 + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '부서가 삭제되었습니다.', + 'action' => 'remove', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '부서가 삭제되었습니다.', + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/DepartmentController.php b/app/Http/Controllers/DepartmentController.php new file mode 100644 index 00000000..56b40eb6 --- /dev/null +++ b/app/Http/Controllers/DepartmentController.php @@ -0,0 +1,48 @@ +departmentService->getActiveDepartments(); + + return view('departments.create', compact('departments')); + } + + /** + * 부서 수정 화면 + */ + public function edit(int $id): View + { + $department = $this->departmentService->getDepartmentById($id); + + if (!$department) { + abort(404, '부서를 찾을 수 없습니다.'); + } + + $departments = $this->departmentService->getActiveDepartments(); + + return view('departments.edit', compact('department', 'departments')); + } +} \ No newline at end of file diff --git a/app/Http/Requests/StoreDepartmentRequest.php b/app/Http/Requests/StoreDepartmentRequest.php new file mode 100644 index 00000000..04789300 --- /dev/null +++ b/app/Http/Requests/StoreDepartmentRequest.php @@ -0,0 +1,72 @@ + [ + 'required', + 'string', + 'max:50', + Rule::unique('departments', 'code') + ->where('tenant_id', $tenantId), + ], + 'name' => 'required|string|max:100', + 'description' => 'nullable|string|max:500', + 'parent_id' => 'nullable|exists:departments,id', + 'is_active' => 'nullable|boolean', + 'sort_order' => 'nullable|integer|min:0', + ]; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes(): array + { + return [ + 'code' => '부서 코드', + 'name' => '부서명', + 'description' => '설명', + 'parent_id' => '상위 부서', + 'is_active' => '활성 상태', + 'sort_order' => '정렬 순서', + ]; + } + + /** + * Get the error messages for the defined validation rules. + */ + public function messages(): array + { + return [ + 'code.required' => '부서 코드는 필수입니다.', + 'code.unique' => '이미 존재하는 부서 코드입니다.', + 'code.max' => '부서 코드는 최대 50자까지 입력 가능합니다.', + 'name.required' => '부서명은 필수입니다.', + 'name.max' => '부서명은 최대 100자까지 입력 가능합니다.', + 'description.max' => '설명은 최대 500자까지 입력 가능합니다.', + 'parent_id.exists' => '유효하지 않은 상위 부서입니다.', + 'sort_order.min' => '정렬 순서는 0 이상이어야 합니다.', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Requests/UpdateDepartmentRequest.php b/app/Http/Requests/UpdateDepartmentRequest.php new file mode 100644 index 00000000..64741e4a --- /dev/null +++ b/app/Http/Requests/UpdateDepartmentRequest.php @@ -0,0 +1,79 @@ +route('id'); // URL 파라미터에서 department ID 가져오기 + + return [ + 'code' => [ + 'required', + 'string', + 'max:50', + Rule::unique('departments', 'code') + ->where('tenant_id', $tenantId) + ->ignore($departmentId), + ], + 'name' => 'required|string|max:100', + 'description' => 'nullable|string|max:500', + 'parent_id' => [ + 'nullable', + 'exists:departments,id', + Rule::notIn([$departmentId]), // 자기 자신을 상위 부서로 설정 불가 + ], + 'is_active' => 'nullable|boolean', + 'sort_order' => 'nullable|integer|min:0', + ]; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes(): array + { + return [ + 'code' => '부서 코드', + 'name' => '부서명', + 'description' => '설명', + 'parent_id' => '상위 부서', + 'is_active' => '활성 상태', + 'sort_order' => '정렬 순서', + ]; + } + + /** + * Get the error messages for the defined validation rules. + */ + public function messages(): array + { + return [ + 'code.required' => '부서 코드는 필수입니다.', + 'code.unique' => '이미 존재하는 부서 코드입니다.', + 'code.max' => '부서 코드는 최대 50자까지 입력 가능합니다.', + 'name.required' => '부서명은 필수입니다.', + 'name.max' => '부서명은 최대 100자까지 입력 가능합니다.', + 'description.max' => '설명은 최대 500자까지 입력 가능합니다.', + 'parent_id.exists' => '유효하지 않은 상위 부서입니다.', + 'parent_id.not_in' => '자기 자신을 상위 부서로 설정할 수 없습니다.', + 'sort_order.min' => '정렬 순서는 0 이상이어야 합니다.', + ]; + } +} \ No newline at end of file diff --git a/app/Services/DepartmentService.php b/app/Services/DepartmentService.php new file mode 100644 index 00000000..1e3bb5cc --- /dev/null +++ b/app/Services/DepartmentService.php @@ -0,0 +1,191 @@ +with('parent'); + + // Tenant 필터링 (선택된 경우에만) + if ($tenantId) { + $query->where('tenant_id', $tenantId); + } + + // 검색 필터 + if (!empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('code', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + }); + } + + // 활성 상태 필터 + if (isset($filters['is_active'])) { + $query->where('is_active', $filters['is_active']); + } + + // 정렬 + $sortBy = $filters['sort_by'] ?? 'sort_order'; + $sortDirection = $filters['sort_direction'] ?? 'asc'; + $query->orderBy($sortBy, $sortDirection); + + return $query->paginate($perPage); + } + + /** + * 특정 부서 조회 + */ + public function getDepartmentById(int $id): ?Department + { + $tenantId = session('selected_tenant_id'); + + $query = Department::query()->with(['parent', 'children']); + + // Tenant 필터링 (선택된 경우에만) + if ($tenantId) { + $query->where('tenant_id', $tenantId); + } + + return $query->find($id); + } + + /** + * 부서 생성 + */ + public function createDepartment(array $data): Department + { + $tenantId = session('selected_tenant_id'); + + $department = Department::create([ + 'tenant_id' => $tenantId, + 'parent_id' => $data['parent_id'] ?? null, + 'code' => $data['code'], + 'name' => $data['name'], + 'description' => $data['description'] ?? null, + 'is_active' => $data['is_active'] ?? true, + 'sort_order' => $data['sort_order'] ?? 0, + 'created_by' => auth()->id(), + ]); + + return $department->fresh(['parent']); + } + + /** + * 부서 수정 + */ + public function updateDepartment(int $id, array $data): bool + { + $department = $this->getDepartmentById($id); + + if (!$department) { + return false; + } + + // 자기 자신을 상위 부서로 설정하는 것 방지 + if (isset($data['parent_id']) && $data['parent_id'] == $id) { + return false; + } + + $updated = $department->update([ + 'parent_id' => $data['parent_id'] ?? $department->parent_id, + 'code' => $data['code'] ?? $department->code, + 'name' => $data['name'] ?? $department->name, + 'description' => $data['description'] ?? $department->description, + 'is_active' => $data['is_active'] ?? $department->is_active, + 'sort_order' => $data['sort_order'] ?? $department->sort_order, + 'updated_by' => auth()->id(), + ]); + + return $updated; + } + + /** + * 부서 삭제 (Soft Delete) + */ + public function deleteDepartment(int $id): bool + { + $department = $this->getDepartmentById($id); + + if (!$department) { + return false; + } + + // 하위 부서가 있는 경우 삭제 불가 + if ($department->children()->count() > 0) { + return false; + } + + $department->deleted_by = auth()->id(); + $department->save(); + + return $department->delete(); + } + + /** + * 부서 코드 중복 체크 + */ + public function isCodeExists(string $code, ?int $excludeId = null): bool + { + $tenantId = session('selected_tenant_id'); + + $query = Department::where('tenant_id', $tenantId) + ->where('code', $code); + + if ($excludeId) { + $query->where('id', '!=', $excludeId); + } + + return $query->exists(); + } + + /** + * 활성 부서 목록 (드롭다운용) + */ + public function getActiveDepartments(): Collection + { + $tenantId = session('selected_tenant_id'); + + $query = Department::query()->where('is_active', true); + + // Tenant 필터링 (선택된 경우에만) + if ($tenantId) { + $query->where('tenant_id', $tenantId); + } + + return $query->orderBy('sort_order')->orderBy('name')->get(['id', 'parent_id', 'code', 'name']); + } + + /** + * 부서 통계 + */ + public function getDepartmentStats(): array + { + $tenantId = session('selected_tenant_id'); + + $baseQuery = Department::query(); + + // Tenant 필터링 (선택된 경우에만) + if ($tenantId) { + $baseQuery->where('tenant_id', $tenantId); + } + + return [ + 'total' => (clone $baseQuery)->count(), + 'active' => (clone $baseQuery)->where('is_active', true)->count(), + 'inactive' => (clone $baseQuery)->where('is_active', false)->count(), + ]; + } +} \ No newline at end of file diff --git a/resources/views/departments/create.blade.php b/resources/views/departments/create.blade.php new file mode 100644 index 00000000..53c256ba --- /dev/null +++ b/resources/views/departments/create.blade.php @@ -0,0 +1,134 @@ +@extends('layouts.app') + +@section('title', '부서 생성') + +@section('content') +
| 부서 코드 | +부서명 | +상위 부서 | +상태 | +정렬순서 | +작업 | +
|---|---|---|---|---|---|
| + {{ $department->code }} + | +
+ {{ $department->name }}
+ @if($department->description)
+ {{ Str::limit($department->description, 50) }}
+ @endif
+ |
+ + {{ $department->parent?->name ?? '-' }} + | ++ @if($department->is_active) + + 활성 + + @else + + 비활성 + + @endif + | ++ {{ $department->sort_order }} + | ++ + 수정 + + + | +
| + 부서가 없습니다. + | +|||||