+
@@ -251,6 +263,9 @@ class="fixed bottom-6 right-6 bg-gray-800 text-white px-4 py-2 rounded-lg shadow
.employee-card.sortable-ghost { opacity: 0.4; }
.employee-card.sortable-drag { box-shadow: 0 8px 25px rgba(0,0,0,0.15); z-index: 999; }
.dept-drop-zone.sortable-drag-over { background-color: #F3E8FF; border-radius: 8px; }
+ .dept-sortable-item.sortable-ghost { opacity: 0.3; }
+ .dept-sortable-item.sortable-drag { box-shadow: 0 8px 25px rgba(0,0,0,0.2); z-index: 999; }
+ .dept-sortable-item.sortable-chosen .dept-drag-handle { background-color: #DDD6FE !important; }
@endsection
@@ -265,6 +280,7 @@ function orgChart() {
searchUnassigned: '',
saving: false,
sortables: [],
+ deptSortables: [],
expandedDepts: [],
get totalEmployees() { return this.employees.length; },
@@ -277,11 +293,15 @@ function orgChart() {
return this.unassignedEmployees.filter(e => e.display_name?.toLowerCase().includes(s));
},
get rootDepartments() {
- return this.departments.filter(d => !d.parent_id);
+ return this.departments
+ .filter(d => !d.parent_id)
+ .sort((a, b) => (a.sort_order ?? 999) - (b.sort_order ?? 999) || a.name.localeCompare(b.name));
},
getChildDepartments(parentId) {
- return this.departments.filter(d => d.parent_id === parentId);
+ return this.departments
+ .filter(d => d.parent_id === parentId)
+ .sort((a, b) => (a.sort_order ?? 999) - (b.sort_order ?? 999) || a.name.localeCompare(b.name));
},
getDeptEmployees(deptId) {
@@ -304,8 +324,10 @@ function orgChart() {
} else {
this.expandedDepts.push(deptId);
}
- // 하위 부서 드롭존 재초기화
- this.$nextTick(() => this.initDropZones());
+ this.$nextTick(() => {
+ this.initDropZones();
+ this.initDeptSortables();
+ });
},
getAvatarColor(name) {
@@ -325,6 +347,7 @@ function orgChart() {
this.$nextTick(() => {
this.initSortable();
this.initDropZones();
+ this.initDeptSortables();
});
},
@@ -356,6 +379,37 @@ function orgChart() {
});
},
+ initDeptSortables() {
+ // 1단계 부서 정렬
+ const rootEl = document.getElementById('root-dept-sortable');
+ if (rootEl && !rootEl._deptSortableInitialized) {
+ rootEl._deptSortableInitialized = true;
+ this.deptSortables.push(new Sortable(rootEl, {
+ handle: '.dept-drag-handle',
+ animation: 200,
+ ghostClass: 'sortable-ghost',
+ dragClass: 'sortable-drag',
+ draggable: '.dept-sortable-item',
+ onEnd: (evt) => this.handleDeptReorder(evt, null),
+ }));
+ }
+
+ // 2단계 부서 정렬
+ document.querySelectorAll('.child-dept-sortable').forEach(el => {
+ if (el._deptSortableInitialized) return;
+ el._deptSortableInitialized = true;
+ const parentId = parseInt(el.dataset.parentId);
+ this.deptSortables.push(new Sortable(el, {
+ handle: '.dept-drag-handle',
+ animation: 200,
+ ghostClass: 'sortable-ghost',
+ dragClass: 'sortable-drag',
+ draggable: '.dept-sortable-item',
+ onEnd: (evt) => this.handleDeptReorder(evt, parentId),
+ }));
+ });
+ },
+
handleDrop(evt) {
const employeeId = parseInt(evt.item.dataset.employeeId);
const targetDeptId = evt.to.dataset.departmentId;
@@ -367,6 +421,24 @@ function orgChart() {
this.saveAssignment(employeeId, newDeptId);
},
+ handleDeptReorder(evt, parentId) {
+ // DOM에서 현재 순서 읽기
+ const container = evt.from;
+ const items = container.querySelectorAll(':scope > .dept-sortable-item');
+ const orders = [];
+
+ items.forEach((item, index) => {
+ const deptId = parseInt(item.dataset.deptId);
+ orders.push({ id: deptId, parent_id: parentId, sort_order: index + 1 });
+
+ // Alpine 데이터도 갱신
+ const dept = this.departments.find(d => d.id === deptId);
+ if (dept) dept.sort_order = index + 1;
+ });
+
+ this.saveDeptOrder(orders);
+ },
+
async saveAssignment(employeeId, departmentId) {
this.saving = true;
try {
@@ -393,6 +465,26 @@ function orgChart() {
}
},
+ async saveDeptOrder(orders) {
+ this.saving = true;
+ try {
+ await fetch('{{ route("rd.org-chart.reorder-depts") }}', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
+ 'Accept': 'application/json',
+ },
+ body: JSON.stringify({ orders }),
+ });
+ if (typeof showToast === 'function') showToast('부서 순서가 저장되었습니다.', 'success');
+ } catch (e) {
+ console.error('부서 순서 저장 실패:', e);
+ } finally {
+ this.saving = false;
+ }
+ },
+
async unassignEmployee(empId) {
const emp = this.employees.find(e => e.id === empId);
if (emp) {
diff --git a/routes/web.php b/routes/web.php
index a84e588a..4db0a7d4 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -392,6 +392,7 @@
Route::post('/org-chart/assign', [RdController::class, 'orgChartAssign'])->name('org-chart.assign');
Route::post('/org-chart/unassign', [RdController::class, 'orgChartUnassign'])->name('org-chart.unassign');
Route::post('/org-chart/reorder', [RdController::class, 'orgChartReorder'])->name('org-chart.reorder');
+ Route::post('/org-chart/reorder-depts', [RdController::class, 'orgChartReorderDepts'])->name('org-chart.reorder-depts');
// 중대재해처벌법 실무 점검
Route::get('/safety-audit', [RdController::class, 'safetyAudit'])->name('safety-audit');