- projects/org-chart/README.md: 아키텍처, API, DB, 프론트엔드 상세 - index_projects.md: 조직도 프로젝트 등록 - INDEX.md: 조직도 문서 링크 추가
10 KiB
조직도 관리 시스템
작성일: 2026-03-06 상태: 🟢 v1.0 구현 완료 프로젝트: MNG 전용 (Blade + Alpine.js + SortableJS)
1. 개요
1.1 목적
테넌트별 조직 구조를 시각적으로 관리하는 트리형 조직도 시스템. 부서 계층 구조와 직원 배치를 드래그 앤 드롭으로 관리한다.
1.2 주요 기능
| 기능 | 설명 |
|---|---|
| 트리형 조직도 | 회사 → 부서 → 하위부서 (무한 depth) 계층 표시 |
| 직원 배치 | 드래그 앤 드롭으로 직원을 부서에 배치/해제 |
| 부서 순서 변경 | 같은 레벨 내 부서 순서 드래그로 변경 |
| 부서 계층 이동 | 부서를 다른 부서 아래로 드래그하여 parent 변경 |
| 부서 숨기기 | 더블클릭 → 숨기기 버튼 → DB 저장 (영구) |
| 임원 필터링 | 대표이사/사장 등은 미배치 목록에서 제외 |
2. 기술 스택
| 구분 | 기술 |
|---|---|
| 백엔드 | Laravel (MNG 프로젝트) |
| 프론트엔드 | Alpine.js + 수동 DOM 렌더링 |
| 드래그 앤 드롭 | SortableJS |
| 스타일 | Tailwind CSS + inline style |
| 데이터 저장 | MySQL departments, employees 테이블 |
3. 아키텍처
3.1 렌더링 방식
핵심: Alpine.js
x-for대신 수동innerHTML렌더링을 사용한다.
SortableJS와 Alpine.js x-for 템플릿이 동시에 DOM을 조작하면 이중 업데이트 버그가 발생한다.
이를 해결하기 위해 부서 트리는 JavaScript로 HTML 문자열을 생성하고 innerHTML로 삽입한다.
Alpine.js 데이터 변경
↓
renderTree() 호출
↓
기존 SortableJS 인스턴스 destroy
↓
buildChildrenHtml(null, 0) → 재귀적 HTML 생성
↓
$refs.deptTree.innerHTML = html
↓
$nextTick → initDeptSortables() + initEmpSortables()
3.2 이벤트 처리
수동 렌더링된 HTML에는 Alpine 디렉티브가 없으므로 이벤트 위임(Event Delegation) 패턴을 사용한다.
루트 div @click="handleClick($event)"
@dblclick="handleDblClick($event)"
↓
e.target.closest('[data-action]') 으로 액션 식별
↓
data-action 값에 따라 분기:
- "unassign" → 직원 미배치
- "hide-dept" → 부서 숨기기
- "restore-dept" → 부서 복원
- "dept-dblclick" → 더블클릭 시 숨기기 버튼 토글
3.3 순환 참조 방지
부서를 자신의 하위로 드래그하면 무한 루프가 발생한다.
isDescendant(ancestorId, targetId) 재귀 함수로 이를 차단한다.
// 드래그 대상(dragId)의 자손인 곳으로는 이동 불가
onMove: (evt) => {
const dragId = parseInt(evt.dragged.dataset.deptId);
const toPid = evt.to.dataset.parentId ? parseInt(evt.to.dataset.parentId) : null;
if (toPid === dragId || this.isDescendant(dragId, toPid)) return false;
}
4. 파일 구조
4.1 MNG 프로젝트
| 파일 | 역할 |
|---|---|
app/Http/Controllers/RdController.php |
컨트롤러 (7개 메서드) |
app/Models/Tenants/Department.php |
부서 모델 (options JSON cast) |
resources/views/rd/org-chart.blade.php |
뷰 (Alpine.js + SortableJS) |
routes/web.php |
라우트 (6개 엔드포인트) |
4.2 API 프로젝트
| 파일 | 역할 |
|---|---|
database/migrations/2026_03_06_201500_add_options_to_departments_table.php |
options JSON 컬럼 추가 |
5. API 엔드포인트
모든 엔드포인트는
rd.네임 프리픽스 하위에 위치한다.
| Method | Route | 컨트롤러 메서드 | 설명 |
|---|---|---|---|
| GET | /rd/org-chart |
orgChart |
조직도 페이지 |
| POST | /rd/org-chart/assign |
orgChartAssign |
직원 부서 배치 |
| POST | /rd/org-chart/unassign |
orgChartUnassign |
직원 부서 해제 |
| POST | /rd/org-chart/reorder |
orgChartReorder |
직원 일괄 이동 |
| POST | /rd/org-chart/reorder-depts |
orgChartReorderDepts |
부서 순서/계층 변경 |
| POST | /rd/org-chart/toggle-hide |
orgChartToggleHide |
부서 숨기기/표시 토글 |
5.1 요청/응답 형식
부서 배치 (POST /rd/org-chart/assign):
{ "employee_id": 1, "department_id": 5 }
→ { "success": true }
부서 순서 변경 (POST /rd/org-chart/reorder-depts):
{
"orders": [
{ "id": 1, "parent_id": null, "sort_order": 1 },
{ "id": 2, "parent_id": null, "sort_order": 2 },
{ "id": 3, "parent_id": 1, "sort_order": 1 }
]
}
→ { "success": true }
부서 숨기기 (POST /rd/org-chart/toggle-hide):
{ "department_id": 5, "hidden": true }
→ { "success": true }
6. DB 구조
6.1 departments 테이블 (관련 컬럼)
| 컬럼 | 타입 | 설명 |
|---|---|---|
id |
int | PK |
tenant_id |
int | 테넌트 FK |
parent_id |
int (nullable) | 상위 부서 (null = 최상위) |
name |
varchar | 부서명 |
code |
varchar | 부서 코드 |
is_active |
bool | 활성 여부 |
sort_order |
int | 정렬 순서 |
options |
json (nullable) | 확장 속성 |
options 키:
| 키 | 타입 | 설명 |
|---|---|---|
orgchart_hidden |
boolean | 조직도에서 숨김 여부 |
6.2 employees 테이블 (관련 컬럼)
| 컬럼 | 타입 | 설명 |
|---|---|---|
id |
int | PK |
tenant_id |
int | 테넌트 FK |
department_id |
int (nullable) | 소속 부서 (null = 미배치) |
display_name |
varchar | 표시 이름 |
position_label |
varchar | 직책/직급 |
employee_status |
enum | active, leave, resigned |
7. 프론트엔드 구현 상세
7.1 Alpine.js 컴포넌트 (orgChart())
데이터:
| 속성 | 타입 | 설명 |
|---|---|---|
departments |
Array | 전체 부서 목록 (서버에서 전달) |
employees |
Array | 전체 직원 목록 (서버에서 전달) |
hiddenDepts |
Set | 숨긴 부서 ID (DB에서 초기화) |
dblClickDept |
int/null | 더블클릭된 부서 ID (숨기기 버튼 표시용) |
execTitles |
Array | 임원 직책 목록 (['대표이사', '사장', '부사장', '회장', '부회장']) |
핵심 메서드:
| 메서드 | 설명 |
|---|---|
renderTree() |
SortableJS 파괴 → HTML 재생성 → SortableJS 재초기화 |
buildChildrenHtml(parentId, level) |
재귀적 자식 부서 HTML 생성 |
buildNodeHtml(dept, level) |
단일 부서 카드 HTML (level별 스타일 차등) |
buildEmpHtml(emp, isLarge) |
직원 카드 HTML |
isDeptHidden(deptId) |
부서 또는 상위 부서가 숨김인지 재귀 체크 |
isDescendant(ancestorId, targetId) |
순환 참조 방지 |
isExecutive(emp) |
임원 여부 판별 |
7.2 SortableJS 그룹
| 그룹 | 대상 | 핸들 | 기능 |
|---|---|---|---|
departments |
.org-children, .org-drop-target |
.dept-drag-handle |
부서 순서/계층 변경 |
employees |
.emp-zone, #unassigned-zone |
(전체) | 직원 배치/해제 |
7.3 CSS 연결선
부서 간 연결선은 CSS ::before/::after 의사 요소로 구현한다.
부모 노드
│ (vertical: div 1px × 24px)
┌───────┼───────┐ (horizontal: ::before + ::after)
│ │ │ (vertical: div 1px × 24px)
자식1 자식2 자식3
| 선택자 | 역할 |
|---|---|
.org-node-wrap 내부 div (1px × 24px) |
세로 연결선 |
.org-node-wrap:not(:first-child)::before |
왼쪽 가로선 (left:0 ~ right:50%) |
.org-node-wrap:not(:last-child)::after |
오른쪽 가로선 (left:50% ~ right:0) |
:only-child |
단일 자식이면 가로선 숨김 |
7.4 부서 숨기기 UX 흐름
① 부서 헤더 더블클릭
↓
② dblClickDept = dept.id → renderTree()
↓
③ 헤더에 빨간 "숨기기" 버튼 표시
↓
④ "숨기기" 클릭
↓
⑤ hiddenDepts.add(id) → renderTree() → POST /toggle-hide (DB 저장)
↓
⑥ 해당 부서 + 하위 부서가 트리에서 제거
⑦ "숨겨진 부서" 패널에 표시
↓
⑧ 패널에서 👁 아이콘 클릭 → hiddenDepts.delete(id) → POST /toggle-hide
7.5 부서 레벨별 스타일
| Level | 색상 테마 | 너비 | 아이콘 |
|---|---|---|---|
| 0 (최상위) | 보라 (#7C3AED) |
200px | ri-building-2-line |
| 1 (중간) | 인디고 (#6366F1) |
180px | ri-git-branch-line |
| 2+ (하위) | 회색 (#6B7280) |
160px | ri-subtract-line |
8. 비즈니스 규칙
8.1 임원 필터링
미배치 직원 목록에서 다음 조건에 해당하면 제외:
position_label이['대표이사', '사장', '부사장', '회장', '부회장']중 하나display_name이 테넌트의ceo_name과 일치
이유: 조직도 최상단에 "대표이사 OOO"이 이미 표시되므로 중복 방지
8.2 부서 숨기기
departments.optionsJSON의orgchart_hidden키로 저장- 숨긴 부서의 하위 부서도 자동으로 숨겨짐 (
isDeptHidden재귀 체크) - 숨겨진 부서 패널에는 직접 숨긴 부서만 표시 (자식은 부모 복원 시 같이 복원)
- 숨기기는 조직도 표시 전용 —
is_active와 무관하며, 부서 데이터에 영향 없음
8.3 직원 표시 형식
- 직책이 있으면:
{직책} {이름}(예: "과장 전진선") - 직책이 없으면:
{이름}(예: "김보곤")
9. 개발 이력
| 날짜 | 커밋 | 내용 |
|---|---|---|
| 2026-03-06 | a12ee886 |
CSS 연결선 수정 + 빈 드롭 타겟 숨김 |
| 2026-03-06 | 9fd72e49 |
부서 숨기기 기능 추가 (프론트 전용) |
| 2026-03-06 | 8c8fd5f6 |
대표이사 미배치 제외 + 숨긴 부서 연결선 제거 |
| 2026-03-06 | 81157a15 |
부서 숨기기 상태 DB 저장 (options.orgchart_hidden) |
관련 문서
- rules/department-tree-api.md — 부서 트리 API 규칙
- rules/employee-api.md — 직원 API 규칙
- system/database/hr.md — HR 테이블 스키마
- standards/options-column-policy.md — options JSON 컬럼 정책
최종 업데이트: 2026-03-06