feat: [org-chart] 부서 숨기기 기능 추가

- 부서 헤더 더블클릭 시 숨기기 버튼 표시
- 숨긴 부서와 하위 부서 트리에서 제거, 연결선 자동 조정
- 숨겨진 부서 패널에서 눈 아이콘 클릭으로 복원
This commit is contained in:
김보곤
2026-03-06 20:15:56 +09:00
parent 3ace66065e
commit 3e47402d3e

View File

@@ -3,7 +3,7 @@
@section('title', '조직도 관리')
@section('content')
<div x-data="orgChart()" x-init="init()" @click="handleClick($event)">
<div x-data="orgChart()" x-init="init()" @click="handleClick($event)" @dblclick="handleDblClick($event)">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
<i class="ri-organization-chart text-purple-600"></i>
@@ -19,7 +19,7 @@
<div class="bg-blue-50 border border-blue-200 rounded-lg px-4 py-3 mb-6 text-sm text-blue-700 flex items-start gap-2">
<i class="ri-information-line text-lg shrink-0 mt-0.5"></i>
<span>직원 카드를 드래그하여 부서에 배치합니다. 부서 헤더를 드래그하여 순서를 변경하거나 다른 부서 아래로 이동할 있습니다.</span>
<span>직원 카드를 드래그하여 부서에 배치합니다. 부서 헤더를 드래그하여 순서를 변경하거나 다른 부서 아래로 이동할 있습니다. 부서 헤더를 <b>더블클릭</b>하면 숨기기 버튼이 나타납니다.</span>
</div>
<!-- 미배치 직원 패널 -->
@@ -43,6 +43,28 @@ class="px-3 py-1 border border-gray-200 rounded text-xs focus:outline-none focus
</div>
</div>
<!-- 숨겨진 부서 패널 -->
<div x-show="hiddenDeptList.length > 0" class="mb-4">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 px-4 py-3">
<div class="flex items-center gap-2 mb-2">
<i class="ri-eye-off-line text-gray-400"></i>
<span class="text-sm font-semibold text-gray-600">숨겨진 부서</span>
<span class="bg-gray-100 text-gray-500 text-xs font-bold px-2 py-0.5 rounded-full" x-text="hiddenDeptList.length"></span>
</div>
<div class="flex flex-wrap gap-2">
<template x-for="dept in hiddenDeptList" :key="dept.id">
<span class="inline-flex items-center gap-1.5 bg-gray-50 border border-gray-200 rounded-lg px-3 py-1.5 text-sm text-gray-700">
<span x-text="dept.name"></span>
<button data-action="restore-dept" :data-dept-id="dept.id"
class="text-purple-500 hover:text-purple-700 transition" title="다시 표시">
<i class="ri-eye-line text-sm"></i>
</button>
</span>
</template>
</div>
</div>
</div>
<!-- 조직도 트리 -->
<div class="overflow-x-auto pb-8">
<div class="org-tree" style="display:flex;flex-direction:column;align-items:center;">
@@ -105,6 +127,8 @@ function orgChart() {
deptSortables: [],
empSortables: [],
unassignedSortable: null,
hiddenDepts: new Set(),
dblClickDept: null,
get totalEmployees() { return this.employees.length; },
get assignedCount() { return this.employees.filter(e => e.department_id).length; },
@@ -187,7 +211,7 @@ function orgChart() {
},
buildChildrenHtml(parentId, level) {
const children = this.getChildrenSorted(parentId);
const children = this.getChildrenSorted(parentId).filter(d => !this.isDeptHidden(d.id));
if (!children.length && parentId !== null) {
return `<div class="org-children org-drop-target" data-parent-id="${parentId}">하위 부서 드롭</div>`;
}
@@ -212,11 +236,14 @@ function orgChart() {
let h = `<div class="org-node-wrap" data-dept-id="${dept.id}">`;
h += `<div style="width:1px;height:24px;background:#D1D5DB;"></div>`;
h += `<div style="width:${s.w};border:${s.border};border-radius:12px;background:#fff;text-align:center;overflow:hidden;">`;
h += `<div class="dept-drag-handle" style="cursor:grab;padding:8px 12px;background:${s.hBg};border-bottom:1px solid ${s.hBdr};display:flex;align-items:center;justify-content:center;gap:4px;">`;
h += `<div class="dept-drag-handle" data-action="dept-dblclick" data-dept-id="${dept.id}" style="cursor:grab;padding:8px 12px;background:${s.hBg};border-bottom:1px solid ${s.hBdr};display:flex;align-items:center;justify-content:center;gap:4px;position:relative;">`;
h += `<i class="ri-drag-move-2-line" style="color:${s.iconClr};opacity:0.5;font-size:11px;"></i>`;
h += `<i class="${s.icon}" style="color:${s.iconClr};font-size:13px;"></i>`;
h += `<span style="font-weight:700;font-size:13px;color:#1F2937;">${this.esc(dept.name)}</span>`;
if (dept.code) h += `<span style="font-size:11px;color:#9CA3AF;">(${this.esc(dept.code)})</span>`;
if (this.dblClickDept === dept.id) {
h += `<button data-action="hide-dept" data-dept-id="${dept.id}" style="position:absolute;right:4px;top:50%;transform:translateY(-50%);background:#EF4444;color:#fff;border:none;border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer;white-space:nowrap;z-index:10;">숨기기</button>`;
}
h += '</div>';
h += `<div class="emp-zone" data-department-id="${dept.id}" style="padding:6px;min-height:36px;">`;
if (emps.length) {
@@ -343,13 +370,52 @@ function orgChart() {
handleClick(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
if (!btn) {
if (this.dblClickDept !== null) { this.dblClickDept = null; this.renderTree(); }
return;
}
if (btn.dataset.action === 'unassign') {
e.preventDefault();
this.unassignEmployee(parseInt(btn.dataset.empId));
} else if (btn.dataset.action === 'hide-dept') {
e.preventDefault(); e.stopPropagation();
this.hideDept(parseInt(btn.dataset.deptId));
} else if (btn.dataset.action === 'restore-dept') {
e.preventDefault();
this.restoreDept(parseInt(btn.dataset.deptId));
}
},
handleDblClick(e) {
const header = e.target.closest('[data-action="dept-dblclick"]');
if (!header) return;
e.preventDefault();
const deptId = parseInt(header.dataset.deptId);
this.dblClickDept = this.dblClickDept === deptId ? null : deptId;
this.renderTree();
},
isDeptHidden(deptId) {
if (this.hiddenDepts.has(deptId)) return true;
const dept = this.departments.find(d => d.id === deptId);
return dept && dept.parent_id ? this.isDeptHidden(dept.parent_id) : false;
},
hideDept(deptId) {
this.hiddenDepts.add(deptId);
this.dblClickDept = null;
this.renderTree();
},
restoreDept(deptId) {
this.hiddenDepts.delete(deptId);
this.renderTree();
},
get hiddenDeptList() {
return this.departments.filter(d => this.hiddenDepts.has(d.id));
},
async saveAssignment(empId, deptId) {
this.saving = true;
try {