- plans/ 폴더 신규 생성 (개발 계획 임시 문서용) - hr-api-react-sync-plan.md를 specs → plans로 이동 - INDEX.md 업데이트 (폴더 구조, 워크플로우) - rules/ HR API 규칙 문서 추가 (employee, attendance, department-tree) - pricing API 요청 문서 업데이트
259 lines
6.9 KiB
Markdown
259 lines
6.9 KiB
Markdown
# Department Tree API (부서트리 조회 API) 규칙
|
|
|
|
## 개요
|
|
|
|
부서트리 API는 테넌트 내 조직도를 계층 구조로 조회하는 API입니다.
|
|
`departments` 테이블의 `parent_id`를 통한 자기참조 관계로 무한 depth 계층 구조를 지원합니다.
|
|
|
|
## 핵심 모델
|
|
|
|
### Department
|
|
|
|
- **위치**: `App\Models\Tenants\Department`
|
|
- **역할**: 부서/조직 정보
|
|
- **특징**:
|
|
- `parent_id` 자기참조로 계층 구조
|
|
- `HasRoles` 트레이트 (부서도 권한/역할 보유 가능)
|
|
- `ModelTrait` 적용 (is_active, 날짜 처리)
|
|
|
|
## 엔드포인트
|
|
|
|
### 부서 트리 전용
|
|
|
|
| Method | Path | 설명 |
|
|
|--------|------|------|
|
|
| GET | `/v1/departments/tree` | 부서 트리 조회 |
|
|
|
|
### 기본 CRUD (참고)
|
|
|
|
| Method | Path | 설명 |
|
|
|--------|------|------|
|
|
| GET | `/v1/departments` | 부서 목록 조회 |
|
|
| GET | `/v1/departments/{id}` | 부서 상세 조회 |
|
|
| POST | `/v1/departments` | 부서 생성 |
|
|
| PATCH | `/v1/departments/{id}` | 부서 수정 |
|
|
| DELETE | `/v1/departments/{id}` | 부서 삭제 |
|
|
|
|
### 부서-사용자 관리
|
|
|
|
| Method | Path | 설명 |
|
|
|--------|------|------|
|
|
| GET | `/v1/departments/{id}/users` | 부서 사용자 목록 |
|
|
| POST | `/v1/departments/{id}/users` | 사용자 배정 |
|
|
| DELETE | `/v1/departments/{id}/users/{user}` | 사용자 제거 |
|
|
| PATCH | `/v1/departments/{id}/users/{user}/primary` | 주부서 설정 |
|
|
|
|
## 데이터 구조
|
|
|
|
### 기본 필드
|
|
|
|
| 필드 | 타입 | 설명 |
|
|
|------|------|------|
|
|
| `id` | int | PK |
|
|
| `tenant_id` | int | 테넌트 ID |
|
|
| `parent_id` | int | 상위 부서 ID (nullable, 최상위는 null) |
|
|
| `code` | string | 부서 코드 (unique) |
|
|
| `name` | string | 부서명 |
|
|
| `description` | string | 부서 설명 |
|
|
| `is_active` | bool | 활성화 상태 |
|
|
| `sort_order` | int | 정렬 순서 |
|
|
| `created_by` | int | 생성자 |
|
|
| `updated_by` | int | 수정자 |
|
|
| `deleted_by` | int | 삭제자 |
|
|
|
|
### 트리 응답 구조
|
|
|
|
```json
|
|
[
|
|
{
|
|
"id": 1,
|
|
"tenant_id": 1,
|
|
"parent_id": null,
|
|
"code": "DEPT001",
|
|
"name": "경영지원본부",
|
|
"is_active": true,
|
|
"sort_order": 1,
|
|
"children": [
|
|
{
|
|
"id": 2,
|
|
"tenant_id": 1,
|
|
"parent_id": 1,
|
|
"code": "DEPT002",
|
|
"name": "인사팀",
|
|
"is_active": true,
|
|
"sort_order": 1,
|
|
"children": [],
|
|
"users": []
|
|
},
|
|
{
|
|
"id": 3,
|
|
"tenant_id": 1,
|
|
"parent_id": 1,
|
|
"code": "DEPT003",
|
|
"name": "재무팀",
|
|
"is_active": true,
|
|
"sort_order": 2,
|
|
"children": [],
|
|
"users": []
|
|
}
|
|
],
|
|
"users": [
|
|
{ "id": 1, "name": "홍길동", "email": "hong@example.com" }
|
|
]
|
|
}
|
|
]
|
|
```
|
|
|
|
## 트리 조회 로직
|
|
|
|
### tree() 메서드 구현
|
|
|
|
```php
|
|
public function tree(array $params = []): array
|
|
{
|
|
// 1. 파라미터 검증
|
|
$withUsers = filter_var($params['with_users'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
|
|
// 2. 최상위 부서 조회 (parent_id가 null)
|
|
$query = Department::query()
|
|
->whereNull('parent_id')
|
|
->orderBy('sort_order')
|
|
->orderBy('name');
|
|
|
|
// 3. 재귀적으로 자식 부서 로드
|
|
$query->with(['children' => function ($q) use ($withUsers) {
|
|
$q->orderBy('sort_order')->orderBy('name');
|
|
$this->loadChildrenRecursive($q, $withUsers);
|
|
}]);
|
|
|
|
// 4. 사용자 포함 옵션
|
|
if ($withUsers) {
|
|
$query->with(['users:id,name,email']);
|
|
}
|
|
|
|
return $query->get()->toArray();
|
|
}
|
|
|
|
// 재귀 로딩 헬퍼
|
|
private function loadChildrenRecursive($query, bool $withUsers): void
|
|
{
|
|
$query->with(['children' => function ($q) use ($withUsers) {
|
|
$q->orderBy('sort_order')->orderBy('name');
|
|
$this->loadChildrenRecursive($q, $withUsers);
|
|
}]);
|
|
|
|
if ($withUsers) {
|
|
$query->with(['users:id,name,email']);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 정렬 규칙
|
|
|
|
1. `sort_order` 오름차순
|
|
2. `name` 오름차순 (동일 sort_order일 때)
|
|
|
|
## 요청 파라미터
|
|
|
|
### GET /v1/departments/tree
|
|
|
|
| 파라미터 | 타입 | 기본값 | 설명 |
|
|
|----------|------|--------|------|
|
|
| `with_users` | bool | false | 부서별 사용자 목록 포함 |
|
|
|
|
### 예시
|
|
|
|
```bash
|
|
# 기본 트리 조회
|
|
GET /v1/departments/tree
|
|
|
|
# 사용자 포함 트리 조회
|
|
GET /v1/departments/tree?with_users=1
|
|
```
|
|
|
|
## 관계 (Relationships)
|
|
|
|
```php
|
|
public function parent(): BelongsTo // 상위 부서
|
|
public function children() // 하위 부서들 (HasMany)
|
|
public function users() // 소속 사용자들 (BelongsToMany)
|
|
public function departmentUsers() // 부서-사용자 pivot (HasMany)
|
|
public function permissionOverrides() // 권한 오버라이드 (MorphMany)
|
|
```
|
|
|
|
## 부서-사용자 관계 (Pivot)
|
|
|
|
### department_user 테이블
|
|
|
|
| 필드 | 타입 | 설명 |
|
|
|------|------|------|
|
|
| `department_id` | int | 부서 ID |
|
|
| `user_id` | int | 사용자 ID |
|
|
| `tenant_id` | int | 테넌트 ID |
|
|
| `is_primary` | bool | 주부서 여부 |
|
|
| `joined_at` | timestamp | 배정일 |
|
|
| `left_at` | timestamp | 해제일 |
|
|
| `deleted_at` | timestamp | Soft Delete |
|
|
|
|
### 주부서 규칙
|
|
|
|
- 한 사용자는 여러 부서에 소속 가능
|
|
- 주부서(`is_primary`)는 사용자당 1개만 가능
|
|
- 주부서 설정 시 기존 주부서는 자동 해제
|
|
|
|
## 권한 관리
|
|
|
|
### 부서 권한 시스템
|
|
|
|
부서는 Spatie Permission과 연동되어 권한을 가질 수 있습니다.
|
|
|
|
- **ALLOW**: `model_has_permissions` 테이블
|
|
- **DENY**: `permission_overrides` 테이블 (effect: -1)
|
|
|
|
### 관련 엔드포인트
|
|
|
|
| Method | Path | 설명 |
|
|
|--------|------|------|
|
|
| GET | `/v1/departments/{id}/permissions` | 부서 권한 목록 |
|
|
| POST | `/v1/departments/{id}/permissions` | 권한 부여/차단 |
|
|
| DELETE | `/v1/departments/{id}/permissions/{permission}` | 권한 제거 |
|
|
|
|
## 주의사항
|
|
|
|
1. **무한 재귀 방지**: Eloquent eager loading으로 처리, 별도 depth 제한 없음
|
|
2. **성능 고려**: 대규모 조직도의 경우 `with_users` 사용 시 응답 시간 증가
|
|
3. **정렬 일관성**: 모든 레벨에서 동일한 정렬 규칙 적용
|
|
4. **멀티테넌트**: tenant_id 기반 자동 스코핑
|
|
5. **주부서 제약**: 사용자당 주부서 1개만 허용
|
|
6. **Soft Delete**: department_user pivot도 Soft Delete 적용
|
|
|
|
## 트리 구축 예시
|
|
|
|
### 조직도 예시
|
|
|
|
```
|
|
경영지원본부 (parent_id: null)
|
|
├── 인사팀 (parent_id: 1)
|
|
│ ├── 채용파트 (parent_id: 2)
|
|
│ └── 교육파트 (parent_id: 2)
|
|
├── 재무팀 (parent_id: 1)
|
|
└── 총무팀 (parent_id: 1)
|
|
|
|
개발본부 (parent_id: null)
|
|
├── 프론트엔드팀 (parent_id: 4)
|
|
├── 백엔드팀 (parent_id: 4)
|
|
└── QA팀 (parent_id: 4)
|
|
```
|
|
|
|
### SQL 예시 (데이터 삽입)
|
|
|
|
```sql
|
|
-- 최상위 부서
|
|
INSERT INTO departments (tenant_id, parent_id, code, name, sort_order)
|
|
VALUES (1, NULL, 'HQ', '경영지원본부', 1);
|
|
|
|
-- 하위 부서
|
|
INSERT INTO departments (tenant_id, parent_id, code, name, sort_order)
|
|
VALUES (1, 1, 'HR', '인사팀', 1);
|
|
```
|