- projects/org-chart/README.md: 아키텍처, API, DB, 프론트엔드 상세 - index_projects.md: 조직도 프로젝트 등록 - INDEX.md: 조직도 문서 링크 추가
318 lines
10 KiB
Markdown
318 lines
10 KiB
Markdown
# 조직도 관리 시스템
|
||
|
||
> **작성일**: 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)` 재귀 함수로 이를 차단한다.
|
||
|
||
```javascript
|
||
// 드래그 대상(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`):
|
||
```json
|
||
{ "employee_id": 1, "department_id": 5 }
|
||
→ { "success": true }
|
||
```
|
||
|
||
**부서 순서 변경** (`POST /rd/org-chart/reorder-depts`):
|
||
```json
|
||
{
|
||
"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`):
|
||
```json
|
||
{ "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.options` JSON의 `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](../../rules/department-tree-api.md) — 부서 트리 API 규칙
|
||
- [rules/employee-api.md](../../rules/employee-api.md) — 직원 API 규칙
|
||
- [system/database/hr.md](../../system/database/hr.md) — HR 테이블 스키마
|
||
- [standards/options-column-policy.md](../../standards/options-column-policy.md) — options JSON 컬럼 정책
|
||
|
||
---
|
||
|
||
**최종 업데이트**: 2026-03-06
|