- features/hr/hr-api-analysis.md 추가 (상세 분석) - features/HR_API_ANALYSIS.md 삭제 (hr 폴더로 이동)
1004 lines
35 KiB
Markdown
1004 lines
35 KiB
Markdown
# HR 관리 API 분석 및 개발 계획
|
||
|
||
> 작성일: 2025-12-08
|
||
> 업데이트: 2025-12-08 (tenant 테이블 분석 및 정책 확정)
|
||
> 목적: React HR 관리 기능과 기존 API/MNG 비교 분석 및 필요 작업 도출
|
||
|
||
---
|
||
|
||
## 📋 분석 개요
|
||
|
||
### React HR 컴포넌트 현황 (커밋 48dbba0)
|
||
|
||
| 기능 | 컴포넌트 위치 | 상태 |
|
||
|------|-------------|------|
|
||
| 사원관리 | `react/src/components/hr/EmployeeManagement/` | Mock 데이터 |
|
||
| 부서관리 | `react/src/components/hr/DepartmentManagement/` | Mock 데이터 |
|
||
| 근태관리 | `react/src/components/hr/AttendanceManagement/` | Mock 데이터 |
|
||
|
||
### 기존 API 현황
|
||
|
||
| 리소스 | 컨트롤러 | 상태 |
|
||
|--------|---------|------|
|
||
| Department | `DepartmentController` | ✅ 존재 (기본 CRUD + 사용자 배정 + 권한) |
|
||
| Employee | - | ❌ 없음 |
|
||
| Attendance | - | ❌ 없음 |
|
||
|
||
### MNG 현황 (참조용 구현체)
|
||
|
||
| 리소스 | 서비스 | 컨트롤러 | 상태 |
|
||
|--------|--------|---------|------|
|
||
| User (사용자) | `UserService.php` (497줄) | `Api/Admin/UserController.php` | ✅ 완전 구현 |
|
||
| Department (부서) | `DepartmentService.php` (249줄) | `Api/Admin/DepartmentController.php` | ✅ 완전 구현 |
|
||
|
||
> **Note**: MNG는 관리자 패널용으로 구현되어 있으며, API 개발 시 참조할 수 있는 완성된 구현체입니다.
|
||
|
||
---
|
||
|
||
## 🔑 핵심 정책: 사용자와 사원 정보 분리
|
||
|
||
### 개념 정의
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ 사용자 (User) - 전역, users 테이블 │
|
||
│ - 시스템 계정 정보 (로그인, 인증) │
|
||
│ - id, user_id, name, email, phone │
|
||
│ - password, is_active, is_super_admin │
|
||
└─────────────────────────────────────────┘
|
||
│
|
||
│ 1:N (테넌트별)
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 사원 (Employee) - 테넌트별 프로필 │
|
||
│ - tenant_user_profiles 테이블 │
|
||
│ - 사원코드, 주민번호, 성별, 주소 │
|
||
│ - 급여, 입사일, 고용형태, 직급, 상태 │
|
||
│ - 부서, 직위, 직책, 상급자 │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### 데이터 모델 전략
|
||
|
||
**JSON 필드 활용 원칙:**
|
||
- **인덱싱 필요 필드** → 컬럼으로 유지 (employee_status)
|
||
- **확장/복잡한 필드** → `json_extra`에 JSON으로 저장
|
||
- **추후 인덱싱 필요시** → MySQL 가상 컬럼 + 인덱스로 보완 가능
|
||
|
||
```
|
||
users 테이블 (전역 - 시스템 계정)
|
||
├── 계정: id, user_id, name, email, phone, password
|
||
├── 상태: is_active, is_super_admin, must_change_password
|
||
├── 인증: email_verified_at, last_login_at, two_factor_*
|
||
└── 기타: options (개인 설정), profile_photo_path
|
||
|
||
tenant_user_profiles 테이블 (테넌트별 - 사원 정보)
|
||
├── 기존 컬럼: department_id, position_key, job_title_key, employment_type_key
|
||
├── 기존 컬럼: manager_user_id, profile_photo_path, display_name
|
||
├── 추가 컬럼: employee_status (인덱싱 필요)
|
||
└── json_extra: employee_code, resident_number, gender, address, salary,
|
||
hire_date, rank, bank_account, work_type, contract_info...
|
||
```
|
||
|
||
**사원 형태 다양성 지원:**
|
||
- 정규직, 계약직, 파트타임, 인턴 (기존 employment_type_key)
|
||
- 일용직, 비정규직, 임시직, 외부 업무 (json_extra.work_type으로 확장)
|
||
- 계약 정보, 외부 파견 정보 등 (json_extra.contract_info)
|
||
|
||
### 상태 필드 구분 (중요!)
|
||
|
||
시스템에는 3가지 상태 개념이 존재합니다:
|
||
|
||
| 필드 | 위치 | 의미 | 레벨 | 값 |
|
||
|------|------|------|------|-----|
|
||
| `users.is_active` | users 테이블 | 시스템 계정 활성화 | 전역 (User) | boolean |
|
||
| `user_tenants.is_active` | pivot 테이블 | 테넌트 멤버십 활성화 | 테넌트별 | boolean |
|
||
| `employee_status` | **tenant_user_profiles** | 고용 상태 | 테넌트별 | 'active'\|'leave'\|'resigned' |
|
||
|
||
```
|
||
홍길동 (user_id: 1)
|
||
├── users.is_active = true (시스템 계정 활성)
|
||
├── 테넌트 A (HQ)
|
||
│ ├── user_tenants.is_active = true (멤버십 활성)
|
||
│ └── tenant_user_profiles.employee_status = 'active' (재직)
|
||
└── 테넌트 B (협력사)
|
||
├── user_tenants.is_active = true (멤버십 활성)
|
||
└── tenant_user_profiles.employee_status = 'resigned' (퇴직)
|
||
```
|
||
|
||
**시스템 동작:**
|
||
```php
|
||
// AuthService.php:42 - 로그인 시 users.is_active 체크
|
||
if (! $user->is_active) {
|
||
Auth::logout();
|
||
$this->loginError = '비활성화된 계정입니다.'; // 전체 시스템 차단
|
||
return false;
|
||
}
|
||
```
|
||
|
||
### 테이블 구조
|
||
|
||
#### 기존 테이블 분석 결과
|
||
|
||
| 테이블 | 목적 | 기존 컬럼 |
|
||
|--------|------|----------|
|
||
| `users` | 시스템 계정 (전역) | id, user_id, name, email, phone, password, is_active, is_super_admin... |
|
||
| `user_tenants` | 멤버십 관리 | user_id, tenant_id, is_active, is_default, joined_at, left_at |
|
||
| `tenant_user_profiles` | 사원 프로필 (테넌트별) | tenant_id, user_id, department_id, position_key, job_title_key, employment_type_key, manager_user_id, json_extra... |
|
||
|
||
#### tenant_user_profiles 확장 (json_extra 활용)
|
||
|
||
**인덱싱 필요 컬럼만 추가:**
|
||
```php
|
||
// tenant_user_profiles 테이블에 추가 (마이그레이션)
|
||
$table->enum('employee_status', ['active', 'leave', 'resigned'])
|
||
->default('active')
|
||
->comment('고용상태 (인덱싱 필요)');
|
||
|
||
$table->index(['tenant_id', 'employee_status']);
|
||
```
|
||
|
||
**나머지 사원 정보는 json_extra에 저장:**
|
||
```php
|
||
// json_extra 구조 예시
|
||
{
|
||
"employee_code": "EMP-001",
|
||
"resident_number": "encrypted_value", // 암호화 저장
|
||
"gender": "male",
|
||
"address": {
|
||
"zipCode": "12345",
|
||
"address1": "서울시 강남구",
|
||
"address2": "테헤란로 123"
|
||
},
|
||
"salary": 50000000,
|
||
"hire_date": "2024-01-15",
|
||
"rank": "대리",
|
||
"bank_account": {
|
||
"bankName": "신한은행",
|
||
"accountNumber": "110-xxx-xxxxxx",
|
||
"accountHolder": "홍길동"
|
||
},
|
||
"work_type": "regular", // regular, daily, temporary, external
|
||
"contract_info": {
|
||
"start_date": "2024-01-15",
|
||
"end_date": null,
|
||
"external_company": null
|
||
}
|
||
}
|
||
```
|
||
|
||
**추후 인덱싱 필요시 (MySQL 가상 컬럼):**
|
||
```sql
|
||
ALTER TABLE tenant_user_profiles
|
||
ADD employee_code_idx VARCHAR(50) AS (JSON_UNQUOTE(json_extra->'$.employee_code')) STORED,
|
||
ADD INDEX idx_employee_code (tenant_id, employee_code_idx);
|
||
```
|
||
|
||
**기존 컬럼 활용:**
|
||
- `employment_type_key`: 고용형태 (정규직/계약직/파트타임/인턴)
|
||
- `position_key`: 직위
|
||
- `job_title_key`: 직책
|
||
- `department_id`: 부서
|
||
- `profile_photo_path`: 프로필 이미지
|
||
|
||
### 장점
|
||
|
||
1. **완전한 분리**: 사용자(전역) vs 사원(테넌트별) 명확한 구분
|
||
2. **역할 분리**: users(계정) / user_tenants(멤버십) / tenant_user_profiles(사원 프로필)
|
||
3. **테넌트별 관리**: 같은 사람이 테넌트별로 다른 사원코드, 급여, 직급 가능
|
||
4. **기존 구조 활용**: tenant_user_profiles 패턴 그대로 확장
|
||
5. **유연한 확장**: json_extra로 추가 속성 자유롭게 확장
|
||
|
||
---
|
||
|
||
## 🔍 상세 분석
|
||
|
||
### 1. 사원관리 (Employee Management)
|
||
|
||
> **정책**: 사용자(users) = 전역 시스템 계정, 사원(tenant_user_profiles) = 테넌트별 인사 정보. 완전 분리.
|
||
|
||
#### React 컴포넌트 타입 정의 (`types.ts`)
|
||
|
||
```typescript
|
||
interface Employee {
|
||
id: string;
|
||
name: string;
|
||
employeeCode?: string;
|
||
residentNumber?: string;
|
||
phone?: string;
|
||
email?: string;
|
||
salary?: number;
|
||
bankAccount?: BankAccount;
|
||
profileImage?: string;
|
||
gender?: Gender; // 'male' | 'female'
|
||
address?: Address;
|
||
hireDate?: string;
|
||
employmentType?: EmploymentType; // 'regular' | 'contract' | 'parttime' | 'intern'
|
||
rank?: string;
|
||
status: EmployeeStatus; // 'active' | 'leave' | 'resigned'
|
||
departmentPositions: DepartmentPosition[];
|
||
userInfo?: UserInfo;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
```
|
||
|
||
#### React ↔ 테이블 매핑
|
||
|
||
**전역 (users 테이블) - 시스템 계정 정보**
|
||
|
||
| React 필드 | 저장 위치 | 비고 |
|
||
|-----------|----------|------|
|
||
| `id` | `users.id` | PK |
|
||
| `name` | `users.name` | 이름 |
|
||
| `phone` | `users.phone` | 전화번호 |
|
||
| `email` | `users.email` | 이메일 |
|
||
| `userInfo.userId` | `users.user_id` | 로그인 ID |
|
||
| `userInfo.accountStatus` | `users.is_active` | 계정 활성화 |
|
||
|
||
**테넌트별 (tenant_user_profiles 테이블) - 사원 정보**
|
||
|
||
| React 필드 | 저장 위치 | 비고 |
|
||
|-----------|----------|------|
|
||
| `profileImage` | `tenant_user_profiles.profile_photo_path` | 기존 컬럼 |
|
||
| `status` | `tenant_user_profiles.employee_status` | 신규 컬럼 (인덱싱) |
|
||
| `employmentType` | `tenant_user_profiles.employment_type_key` | 기존 컬럼 |
|
||
| `departmentPositions` | `department_id` + `position_key` | 기존 컬럼 |
|
||
| `employeeCode` | `json_extra.employee_code` | JSON |
|
||
| `residentNumber` | `json_extra.resident_number` | JSON (암호화) |
|
||
| `gender` | `json_extra.gender` | JSON |
|
||
| `address` | `json_extra.address` | JSON (객체) |
|
||
| `salary` | `json_extra.salary` | JSON |
|
||
| `hireDate` | `json_extra.hire_date` | JSON |
|
||
| `rank` | `json_extra.rank` | JSON |
|
||
| `bankAccount` | `json_extra.bank_account` | JSON (객체) |
|
||
| `workType` | `json_extra.work_type` | JSON (일용직/외부 등) |
|
||
| `contractInfo` | `json_extra.contract_info` | JSON (계약정보) |
|
||
|
||
#### 필요한 API 엔드포인트
|
||
|
||
| Method | Endpoint | 설명 | 우선순위 |
|
||
|--------|----------|------|---------|
|
||
| GET | `/v1/employees` | 사원 목록 (검색, 필터, 페이지네이션) | 🔴 필수 |
|
||
| GET | `/v1/employees/{id}` | 사원 상세 | 🔴 필수 |
|
||
| POST | `/v1/employees` | 사원 등록 (users 테이블에 생성) | 🔴 필수 |
|
||
| PATCH | `/v1/employees/{id}` | 사원 수정 | 🔴 필수 |
|
||
| DELETE | `/v1/employees/{id}` | 사원 삭제 (soft delete) | 🔴 필수 |
|
||
| POST | `/v1/employees/bulk-delete` | 일괄 삭제 | 🟡 권장 |
|
||
| POST | `/v1/employees/csv-upload` | CSV 일괄 등록 | 🟡 권장 |
|
||
| POST | `/v1/employees/{id}/create-account` | 시스템 계정 생성 (password 설정) | 🟢 선택 |
|
||
| GET | `/v1/employees/stats` | 통계 (재직/휴직/퇴직/평균근속) | 🟢 선택 |
|
||
|
||
#### 필터/검색 파라미터
|
||
|
||
```yaml
|
||
GET /v1/employees:
|
||
q: string # 이름, 사원코드, 이메일 검색
|
||
status: string # active, leave, resigned (tenant_user_profiles.employee_status)
|
||
department_id: int
|
||
has_account: bool # 시스템 계정 보유 여부 (password IS NOT NULL)
|
||
page: int
|
||
per_page: int
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 부서관리 (Department Management)
|
||
|
||
#### React 컴포넌트 타입 정의 (`types.ts`)
|
||
|
||
```typescript
|
||
interface Department {
|
||
id: number;
|
||
name: string;
|
||
parentId: number | null;
|
||
depth: number; // 깊이 (0: 최상위, 무제한)
|
||
children?: Department[]; // 하위 부서 (재귀)
|
||
}
|
||
```
|
||
|
||
#### 기존 API vs React 요구사항
|
||
|
||
| 항목 | 기존 API | React 요구사항 | 갭 |
|
||
|------|---------|---------------|-----|
|
||
| 목록 조회 | 플랫 리스트 | 트리 구조 | ⚠️ 변경 필요 |
|
||
| parentId | ❌ 없음 | ✅ 필요 | ⚠️ 추가 필요 |
|
||
| depth | ❌ 없음 | ✅ 필요 | ⚠️ 추가 필요 |
|
||
| children | ❌ 없음 | ✅ 필요 | ⚠️ 추가 필요 |
|
||
|
||
#### 기존 API 엔드포인트 (수정 필요)
|
||
|
||
```yaml
|
||
# 현재
|
||
GET /v1/departments:
|
||
- 플랫 리스트 반환
|
||
- code, name, description, is_active, sort_order
|
||
|
||
# 필요한 변경
|
||
GET /v1/departments:
|
||
- 트리 구조 반환 (parent_id, depth, children 포함)
|
||
- 또는 ?tree=true 파라미터로 트리 반환 옵션
|
||
```
|
||
|
||
#### 필요한 변경사항
|
||
|
||
| 항목 | 작업 내용 | 우선순위 |
|
||
|------|---------|---------|
|
||
| DB 스키마 | `departments` 테이블에 `parent_id`, `depth` 컬럼 추가 | 🔴 필수 |
|
||
| Model | Department 모델에 parent/children 관계 추가 | 🔴 필수 |
|
||
| Service | 트리 구조 빌드 로직 추가 | 🔴 필수 |
|
||
| Controller | 트리 조회 옵션 추가 | 🔴 필수 |
|
||
|
||
---
|
||
|
||
### 3. 근태관리 (Attendance Management)
|
||
|
||
**복잡한 출퇴근 시나리오 지원:**
|
||
- GPS 자동 출퇴근 (위치 기반 자동 체크인/아웃)
|
||
- 다양한 사원 형태: 일용직, 비정규직, 임시직, 외부 업무
|
||
- 외부 근무: 출장, 파견, 현장 업무, 재택근무
|
||
- 복수 출퇴근: 하루 여러 번 출입 기록 가능
|
||
|
||
#### React 컴포넌트 타입 정의 (`types.ts`)
|
||
|
||
```typescript
|
||
interface AttendanceRecord {
|
||
id: string;
|
||
employeeId: string;
|
||
employeeName: string;
|
||
department: string;
|
||
position: string;
|
||
rank: string;
|
||
baseDate: string;
|
||
checkIn: string | null;
|
||
checkOut: string | null;
|
||
breakTime: string | null;
|
||
overtimeHours: string | null;
|
||
reason: {
|
||
type: ReasonType;
|
||
label: string;
|
||
documentId?: string;
|
||
} | null;
|
||
status: AttendanceStatus;
|
||
// GPS/위치 정보 (json_details에 저장)
|
||
gpsData?: {
|
||
checkInLocation?: { lat: number; lng: number; address?: string };
|
||
checkOutLocation?: { lat: number; lng: number; address?: string };
|
||
isAutoChecked?: boolean; // GPS 자동 체크인 여부
|
||
};
|
||
// 외부 근무 정보
|
||
externalWork?: {
|
||
type: 'dispatch' | 'fieldWork' | 'remote';
|
||
location?: string;
|
||
company?: string; // 파견처
|
||
};
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
type AttendanceStatus =
|
||
'onTime' | 'late' | 'absent' | 'vacation' |
|
||
'businessTrip' | 'fieldWork' | 'overtime' | 'remote';
|
||
|
||
type ReasonType =
|
||
'businessTripRequest' | 'vacationRequest' |
|
||
'fieldWorkRequest' | 'overtimeRequest' | 'remoteRequest';
|
||
```
|
||
|
||
#### 필요한 API 엔드포인트
|
||
|
||
| Method | Endpoint | 설명 | 우선순위 |
|
||
|--------|----------|------|---------|
|
||
| GET | `/v1/attendances` | 근태 목록 (날짜범위, 필터, 페이지네이션) | 🔴 필수 |
|
||
| GET | `/v1/attendances/{id}` | 근태 상세 | 🟡 권장 |
|
||
| POST | `/v1/attendances` | 근태 등록 | 🔴 필수 |
|
||
| PATCH | `/v1/attendances/{id}` | 근태 수정 | 🔴 필수 |
|
||
| POST | `/v1/attendances/reason` | 사유 등록 (문서 연결) | 🟡 권장 |
|
||
| GET | `/v1/attendances/stats` | 통계 (정시/지각/결근/휴가) | 🟢 선택 |
|
||
| GET | `/v1/attendances/export` | 엑셀 다운로드 | 🟢 선택 |
|
||
|
||
#### 필터/검색 파라미터
|
||
|
||
```yaml
|
||
GET /v1/attendances:
|
||
q: string # 이름, 부서 검색
|
||
status: string # onTime, late, absent, vacation, etc.
|
||
date_from: date # 시작일
|
||
date_to: date # 종료일
|
||
department_id: int
|
||
sort_by: string # rank, department, name
|
||
sort_dir: string # asc, desc
|
||
page: int
|
||
per_page: int
|
||
```
|
||
|
||
---
|
||
|
||
## 🏢 MNG 구현체 분석
|
||
|
||
### 1. MNG UserService (사용자관리)
|
||
|
||
MNG에 이미 구현된 사용자 관리 기능으로, Employee API 개발 시 참조할 수 있습니다.
|
||
|
||
#### 구현된 기능
|
||
|
||
| 기능 | 메서드 | 설명 |
|
||
|------|--------|------|
|
||
| 목록 조회 | `getUsers()` | 페이지네이션, 검색, 필터, Soft Delete 필터 |
|
||
| 상세 조회 | `getUserById()` | 단일 사용자 조회 |
|
||
| 모달 조회 | `getUserForModal()` | 역할/부서/권한 카운트 포함 |
|
||
| 생성 | `createUser()` | 비밀번호 자동생성/입력, 메일 발송, 역할/부서 동기화 |
|
||
| 수정 | `updateUser()` | 비밀번호 선택적 업데이트, 역할/부서 동기화 |
|
||
| 삭제 | `deleteUser()` | Soft Delete |
|
||
| 복원 | `restoreUser()` | Soft Delete 복원 |
|
||
| 영구삭제 | `forceDeleteUser()` | 아카이브 저장 후 영구 삭제 |
|
||
| 비밀번호 초기화 | `resetPassword()` | 임의 비밀번호 생성 + 메일 발송 |
|
||
| 역할 동기화 | `syncRoles()` | 테넌트별 역할 관리 |
|
||
| 부서 동기화 | `syncDepartments()` | 테넌트별 부서 관리 |
|
||
|
||
#### 핵심 코드 패턴
|
||
|
||
```php
|
||
// 멀티테넌시 패턴
|
||
$tenantId = session('selected_tenant_id');
|
||
|
||
// 역할/부서 Eager Loading (테넌트별)
|
||
$query->with([
|
||
'userRoles' => fn ($q) => $q->where('tenant_id', $tenantId)->with('role'),
|
||
'departmentUsers' => fn ($q) => $q->where('tenant_id', $tenantId)->with('department'),
|
||
]);
|
||
|
||
// 슈퍼관리자 보호
|
||
if (! auth()->user()?->is_super_admin) {
|
||
$query->where('is_super_admin', false);
|
||
}
|
||
```
|
||
|
||
#### React Employee vs MNG User 매핑
|
||
|
||
| React Employee 필드 | MNG User 필드 | 차이점 |
|
||
|---------------------|---------------|--------|
|
||
| `id` | `id` | 동일 |
|
||
| `name` | `name` | 동일 |
|
||
| `email` | `email` | 동일 |
|
||
| `phone` | `phone` | 동일 |
|
||
| `status` (active/leave/resigned) | `is_active` (boolean) | ⚠️ 확장 필요 |
|
||
| `employeeCode` | ❌ 없음 | ➕ 추가 필요 |
|
||
| `residentNumber` | ❌ 없음 | ➕ 추가 필요 |
|
||
| `salary` | ❌ 없음 | ➕ 추가 필요 |
|
||
| `bankAccount` | ❌ 없음 | ➕ 추가 필요 |
|
||
| `hireDate` | ❌ 없음 | ➕ 추가 필요 |
|
||
| `employmentType` | ❌ 없음 | ➕ 추가 필요 |
|
||
| `rank` | ❌ 없음 | ➕ 추가 필요 |
|
||
| `departmentPositions[]` | `departmentUsers` | ⚠️ position 추가 필요 |
|
||
| `userInfo` (userId, role) | 기본 필드 | User 자체가 계정 정보 |
|
||
|
||
---
|
||
|
||
### 2. MNG DepartmentService (부서관리)
|
||
|
||
MNG에 이미 구현된 부서 관리 기능으로, 트리 구조를 지원합니다.
|
||
|
||
#### 구현된 기능
|
||
|
||
| 기능 | 메서드 | 설명 |
|
||
|------|--------|------|
|
||
| 목록 조회 | `getDepartments()` | 페이지네이션, 검색, 필터, parent 포함 |
|
||
| 상세 조회 | `getDepartmentById()` | parent/children 관계 포함 |
|
||
| 생성 | `createDepartment()` | parent_id 지원 |
|
||
| 수정 | `updateDepartment()` | 자기참조 방지 로직 포함 |
|
||
| 삭제 | `deleteDepartment()` | 하위 부서 보호, Soft Delete |
|
||
| 복원 | `restoreDepartment()` | Soft Delete 복원 |
|
||
| 영구삭제 | `forceDeleteDepartment()` | 하위 부서 보호 |
|
||
| 코드 중복 체크 | `isCodeExists()` | 테넌트별 코드 유니크 검사 |
|
||
| 활성 목록 | `getActiveDepartments()` | 드롭다운용 (id, parent_id, code, name) |
|
||
| 통계 | `getDepartmentStats()` | total, active, inactive |
|
||
|
||
#### 핵심 코드 패턴
|
||
|
||
```php
|
||
// 트리 구조 관계 정의 (Model)
|
||
public function parent(): BelongsTo
|
||
{
|
||
return $this->belongsTo(self::class, 'parent_id');
|
||
}
|
||
|
||
public function children(): HasMany
|
||
{
|
||
return $this->hasMany(self::class, 'parent_id');
|
||
}
|
||
|
||
// 하위 부서 보호 (삭제 시)
|
||
if ($department->children()->count() > 0) {
|
||
return false;
|
||
}
|
||
|
||
// 자기 참조 방지
|
||
if (isset($data['parent_id']) && $data['parent_id'] == $id) {
|
||
return false;
|
||
}
|
||
```
|
||
|
||
#### React Department vs MNG Department 매핑
|
||
|
||
| React Department 필드 | MNG Department 필드 | 상태 |
|
||
|-----------------------|---------------------|------|
|
||
| `id` | `id` | ✅ 동일 |
|
||
| `name` | `name` | ✅ 동일 |
|
||
| `parentId` | `parent_id` | ✅ 동일 |
|
||
| `depth` | ❌ 없음 | ➕ 추가 필요 (계산 가능) |
|
||
| `children[]` | `children()` 관계 | ✅ 동일 |
|
||
| - | `code` | MNG 추가 필드 |
|
||
| - | `description` | MNG 추가 필드 |
|
||
| - | `is_active` | MNG 추가 필드 |
|
||
| - | `sort_order` | MNG 추가 필드 |
|
||
|
||
---
|
||
|
||
## 🔗 통합 개발 전략
|
||
|
||
### 옵션 A: API에서 새로 구현 (권장)
|
||
|
||
MNG 코드를 **참조**하여 API에 새로운 Employee/Attendance 리소스 구현
|
||
|
||
**장점**:
|
||
- API 규칙(FormRequest, Swagger, i18n) 완전 준수
|
||
- React 요구사항에 최적화된 설계
|
||
- 독립적인 개발/테스트 가능
|
||
|
||
**단점**:
|
||
- 개발 시간 소요
|
||
- 일부 코드 중복
|
||
|
||
### 개발 방안: MNG 참조하여 API 구현
|
||
|
||
1. **DepartmentService 패턴 참조**: 트리 구조 로직 (parent/children, 자기참조 방지, 하위 보호)
|
||
2. **UserService 패턴 참조**: 역할/부서 동기화, 비밀번호 관리, 아카이브
|
||
3. **API 규칙 적용**: FormRequest, Swagger, BelongsToTenant, 감사 로그
|
||
|
||
---
|
||
|
||
## 📊 작업 계획
|
||
|
||
### Phase 1: 데이터베이스 스키마 (0.5일)
|
||
|
||
#### 1.1 마이그레이션 파일 생성
|
||
|
||
```bash
|
||
# 부서 테이블 수정 (트리 구조)
|
||
php artisan make:migration add_tree_fields_to_departments_table
|
||
|
||
# tenant_user_profiles 테이블 수정 (사원 정보 확장)
|
||
php artisan make:migration add_employee_fields_to_tenant_user_profiles_table
|
||
|
||
# 근태 테이블 생성
|
||
php artisan make:migration create_attendances_table
|
||
```
|
||
|
||
#### 1.2 tenant_user_profiles 테이블 확장 (인덱싱 필드만)
|
||
|
||
```php
|
||
Schema::table('tenant_user_profiles', function (Blueprint $table) {
|
||
// 인덱싱 필요한 필드만 컬럼으로 추가
|
||
$table->enum('employee_status', ['active', 'leave', 'resigned'])
|
||
->default('active')
|
||
->comment('고용상태 (인덱싱 필요)');
|
||
|
||
$table->index(['tenant_id', 'employee_status']);
|
||
|
||
// 나머지 사원 정보는 기존 json_extra 컬럼 활용
|
||
// json_extra 구조:
|
||
// {
|
||
// "employee_code": "EMP-001",
|
||
// "resident_number": "암호화된값",
|
||
// "gender": "male",
|
||
// "address": { "zipCode": "", "address1": "", "address2": "" },
|
||
// "salary": 50000000,
|
||
// "hire_date": "2024-01-15",
|
||
// "rank": "대리",
|
||
// "bank_account": { "bankName": "", "accountNumber": "", "accountHolder": "" },
|
||
// "work_type": "regular", // regular, daily, temporary, external
|
||
// "contract_info": { "start_date": "", "end_date": null, "external_company": null }
|
||
// }
|
||
});
|
||
```
|
||
|
||
#### 1.3 attendances 테이블 스키마 (json_details 활용)
|
||
|
||
**인덱싱 필요 필드만 컬럼으로, 나머지는 JSON:**
|
||
|
||
```php
|
||
Schema::create('attendances', function (Blueprint $table) {
|
||
$table->id();
|
||
|
||
// 인덱싱 필요 필드 (컬럼)
|
||
$table->unsignedBigInteger('tenant_id');
|
||
$table->unsignedBigInteger('user_id');
|
||
$table->date('base_date');
|
||
$table->enum('status', [
|
||
'onTime', 'late', 'absent', 'vacation',
|
||
'businessTrip', 'fieldWork', 'overtime', 'remote'
|
||
])->default('onTime');
|
||
|
||
// 확장 가능한 상세 정보 (JSON)
|
||
$table->json('json_details')->nullable()->comment('출퇴근 상세 정보');
|
||
|
||
// 감사 로그
|
||
$table->unsignedBigInteger('created_by')->nullable();
|
||
$table->unsignedBigInteger('updated_by')->nullable();
|
||
$table->timestamps();
|
||
$table->softDeletes();
|
||
|
||
// 인덱스
|
||
$table->foreign('tenant_id')->references('id')->on('tenants');
|
||
$table->foreign('user_id')->references('id')->on('users');
|
||
$table->unique(['tenant_id', 'user_id', 'base_date']);
|
||
$table->index(['tenant_id', 'base_date', 'status']);
|
||
});
|
||
```
|
||
|
||
**json_details 구조:**
|
||
```json
|
||
{
|
||
"check_in": "09:00:00",
|
||
"check_out": "18:00:00",
|
||
"break_time": "01:00",
|
||
"overtime_hours": "02:00",
|
||
"reason": {
|
||
"type": "vacationRequest",
|
||
"label": "연차",
|
||
"document_id": "DOC-001"
|
||
},
|
||
"gps_data": {
|
||
"check_in_location": { "lat": 37.5665, "lng": 126.9780, "address": "서울시 중구" },
|
||
"check_out_location": { "lat": 37.5665, "lng": 126.9780, "address": "서울시 중구" },
|
||
"is_auto_checked": true
|
||
},
|
||
"external_work": {
|
||
"type": "dispatch",
|
||
"location": "협력사 A",
|
||
"company": "ABC Corp"
|
||
},
|
||
"multiple_entries": [
|
||
{ "in": "09:00", "out": "12:00" },
|
||
{ "in": "13:00", "out": "18:00" }
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 2: 사원관리 API (1.5일)
|
||
|
||
> **정책**: User + TenantUserProfile 모델 활용. 사원 정보는 tenant_user_profiles 테이블에 저장.
|
||
|
||
#### 2.1 파일 생성/수정 목록
|
||
|
||
```
|
||
api/app/Models/Tenants/TenantUserProfile.php (수정: 사원 필드 확장)
|
||
api/app/Services/EmployeeService.php (신규: 사원 CRUD, MNG UserService 참조)
|
||
api/app/Http/Controllers/Api/V1/EmployeeController.php (신규)
|
||
api/app/Http/Requests/Employee/IndexRequest.php
|
||
api/app/Http/Requests/Employee/StoreRequest.php
|
||
api/app/Http/Requests/Employee/UpdateRequest.php
|
||
api/app/Swagger/v1/EmployeeApi.php
|
||
```
|
||
|
||
#### 2.2 TenantUserProfile 모델 확장 (json_extra 활용)
|
||
|
||
```php
|
||
// api/app/Models/Tenants/TenantUserProfile.php에 추가
|
||
|
||
// fillable - employee_status만 신규 컬럼
|
||
protected $fillable = [
|
||
'tenant_id', 'user_id', 'department_id',
|
||
'position_key', 'job_title_key', 'work_location_key', 'employment_type_key',
|
||
'manager_user_id', 'json_extra', 'profile_photo_path', 'display_name',
|
||
'employee_status', // 신규 컬럼 (인덱싱 필요)
|
||
];
|
||
|
||
// casts - json_extra를 array로
|
||
protected $casts = [
|
||
'json_extra' => 'array',
|
||
];
|
||
|
||
// json_extra 헬퍼 메서드
|
||
public function getEmployeeCodeAttribute(): ?string
|
||
{
|
||
return $this->json_extra['employee_code'] ?? null;
|
||
}
|
||
|
||
public function getSalaryAttribute(): ?float
|
||
{
|
||
return $this->json_extra['salary'] ?? null;
|
||
}
|
||
|
||
public function getHireDateAttribute(): ?string
|
||
{
|
||
return $this->json_extra['hire_date'] ?? null;
|
||
}
|
||
|
||
public function getRankAttribute(): ?string
|
||
{
|
||
return $this->json_extra['rank'] ?? null;
|
||
}
|
||
|
||
public function getBankAccountAttribute(): ?array
|
||
{
|
||
return $this->json_extra['bank_account'] ?? null;
|
||
}
|
||
|
||
public function getWorkTypeAttribute(): ?string
|
||
{
|
||
return $this->json_extra['work_type'] ?? 'regular';
|
||
}
|
||
|
||
// json_extra 일괄 업데이트
|
||
public function updateEmployeeInfo(array $data): void
|
||
{
|
||
$jsonExtra = $this->json_extra ?? [];
|
||
$allowedKeys = [
|
||
'employee_code', 'resident_number', 'gender', 'address',
|
||
'salary', 'hire_date', 'rank', 'bank_account',
|
||
'work_type', 'contract_info'
|
||
];
|
||
|
||
foreach ($allowedKeys as $key) {
|
||
if (isset($data[$key])) {
|
||
$jsonExtra[$key] = $data[$key];
|
||
}
|
||
}
|
||
|
||
$this->json_extra = $jsonExtra;
|
||
}
|
||
|
||
// 사용자 관계
|
||
public function user(): BelongsTo
|
||
{
|
||
return $this->belongsTo(User::class);
|
||
}
|
||
|
||
// 시스템 계정 보유 여부
|
||
public function getHasAccountAttribute(): bool
|
||
{
|
||
return $this->user?->password !== null;
|
||
}
|
||
```
|
||
|
||
#### 2.3 라우트 추가 (`routes/api.php`)
|
||
|
||
```php
|
||
Route::prefix('employees')->group(function () {
|
||
Route::get('', [EmployeeController::class, 'index'])->name('v1.employees.index');
|
||
Route::post('', [EmployeeController::class, 'store'])->name('v1.employees.store');
|
||
Route::get('/stats', [EmployeeController::class, 'stats'])->name('v1.employees.stats');
|
||
Route::get('/{id}', [EmployeeController::class, 'show'])->name('v1.employees.show');
|
||
Route::patch('/{id}', [EmployeeController::class, 'update'])->name('v1.employees.update');
|
||
Route::delete('/{id}', [EmployeeController::class, 'destroy'])->name('v1.employees.destroy');
|
||
Route::post('/bulk-delete', [EmployeeController::class, 'bulkDelete'])->name('v1.employees.bulkDelete');
|
||
Route::post('/csv-upload', [EmployeeController::class, 'csvUpload'])->name('v1.employees.csvUpload');
|
||
Route::post('/{id}/create-account', [EmployeeController::class, 'createAccount'])->name('v1.employees.createAccount');
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 3: 부서관리 API 수정 (1일)
|
||
|
||
#### 3.1 수정 파일 목록
|
||
|
||
```
|
||
api/database/migrations/xxxx_add_tree_fields_to_departments_table.php
|
||
api/app/Models/Tenants/Department.php (수정)
|
||
api/app/Services/DepartmentService.php (수정)
|
||
api/app/Swagger/v1/DepartmentApi.php (수정)
|
||
```
|
||
|
||
#### 3.2 핵심 변경사항
|
||
|
||
```php
|
||
// Department.php
|
||
public function parent(): BelongsTo
|
||
{
|
||
return $this->belongsTo(self::class, 'parent_id');
|
||
}
|
||
|
||
public function children(): HasMany
|
||
{
|
||
return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order');
|
||
}
|
||
|
||
// DepartmentService.php - 트리 빌드 메서드 추가
|
||
public function getTree(): array
|
||
{
|
||
$departments = Department::with('children')
|
||
->whereNull('parent_id')
|
||
->orderBy('sort_order')
|
||
->get();
|
||
|
||
return $this->buildTree($departments);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 4: 근태관리 API (2일)
|
||
|
||
#### 4.1 파일 생성 목록
|
||
|
||
```
|
||
api/app/Models/Members/Attendance.php
|
||
api/app/Services/AttendanceService.php
|
||
api/app/Http/Controllers/Api/V1/AttendanceController.php
|
||
api/app/Http/Requests/Attendance/IndexRequest.php
|
||
api/app/Http/Requests/Attendance/StoreRequest.php
|
||
api/app/Http/Requests/Attendance/UpdateRequest.php
|
||
api/app/Swagger/v1/AttendanceApi.php
|
||
```
|
||
|
||
#### 4.2 라우트 추가 (`routes/api.php`)
|
||
|
||
```php
|
||
Route::prefix('attendances')->group(function () {
|
||
Route::get('', [AttendanceController::class, 'index'])->name('v1.attendances.index');
|
||
Route::post('', [AttendanceController::class, 'store'])->name('v1.attendances.store');
|
||
Route::get('/stats', [AttendanceController::class, 'stats'])->name('v1.attendances.stats');
|
||
Route::get('/export', [AttendanceController::class, 'export'])->name('v1.attendances.export');
|
||
Route::get('/{id}', [AttendanceController::class, 'show'])->name('v1.attendances.show');
|
||
Route::patch('/{id}', [AttendanceController::class, 'update'])->name('v1.attendances.update');
|
||
Route::post('/reason', [AttendanceController::class, 'storeReason'])->name('v1.attendances.storeReason');
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 📌 체크리스트
|
||
|
||
### Phase 1: 데이터베이스 스키마
|
||
- [ ] departments 테이블에 parent_id, depth 추가
|
||
- [ ] tenant_user_profiles 테이블에 employee_status 컬럼 추가 (인덱싱 필요)
|
||
- [ ] tenant_user_profiles 인덱스 추가: (tenant_id, employee_status)
|
||
- [ ] attendances 테이블 생성 (json_details 포함)
|
||
- [ ] attendances 인덱스 추가: (tenant_id, base_date, status)
|
||
- [ ] 마이그레이션 실행 및 검증
|
||
|
||
### Phase 2: 사원관리 API
|
||
- [ ] TenantUserProfile 모델: employee_status fillable 추가
|
||
- [ ] TenantUserProfile 모델: json_extra 헬퍼 메서드 추가
|
||
- [ ] TenantUserProfile 모델: updateEmployeeInfo() 메서드 추가
|
||
- [ ] EmployeeService 구현 (MNG UserService 참조)
|
||
- [ ] EmployeeController 구현
|
||
- [ ] FormRequest 생성 (Index, Store, Update)
|
||
- [ ] Swagger 문서 작성
|
||
- [ ] 라우트 등록
|
||
- [ ] Pint 실행
|
||
- [ ] 테스트
|
||
|
||
### Phase 3: 부서관리 API 수정
|
||
- [ ] Department 모델에 parent/children 관계 추가
|
||
- [ ] DepartmentService에 트리 빌드 로직 추가
|
||
- [ ] 트리 조회 엔드포인트 추가/수정
|
||
- [ ] Swagger 문서 업데이트
|
||
- [ ] 테스트
|
||
|
||
### Phase 4: 근태관리 API
|
||
- [ ] Attendance 모델 생성 (json_details casts 포함)
|
||
- [ ] Attendance 모델: json_details 헬퍼 메서드 추가
|
||
- [ ] AttendanceService 구현 (GPS 자동 체크인 로직)
|
||
- [ ] AttendanceController 구현
|
||
- [ ] FormRequest 생성 (Index, Store, Update)
|
||
- [ ] Swagger 문서 작성
|
||
- [ ] 라우트 등록
|
||
- [ ] 엑셀 Export 기능
|
||
- [ ] Pint 실행
|
||
- [ ] 테스트
|
||
|
||
---
|
||
|
||
## 🔗 관련 문서
|
||
|
||
- **React 커밋**: `48dbba0` - feat: 단가관리 페이지 마이그레이션 및 HR 관리 기능 추가
|
||
- **기존 API 규칙**: `docs/reference/api-rules.md`
|
||
- **품질 체크리스트**: `docs/reference/quality-checklist.md`
|
||
|
||
### MNG 참조 파일
|
||
|
||
| 파일 | 경로 | 용도 |
|
||
|------|------|------|
|
||
| UserService | `mng/app/Services/UserService.php` | Employee 서비스 참조 |
|
||
| DepartmentService | `mng/app/Services/DepartmentService.php` | 트리 구조 로직 참조 |
|
||
| UserController (API) | `mng/app/Http/Controllers/Api/Admin/UserController.php` | API 컨트롤러 패턴 |
|
||
| DepartmentController (API) | `mng/app/Http/Controllers/Api/Admin/DepartmentController.php` | 트리 API 패턴 |
|
||
| User Model | `mng/app/Models/User.php` | 관계 정의 참조 |
|
||
| Department Model | `mng/app/Models/Tenants/Department.php` | 트리 관계 정의 |
|
||
|
||
---
|
||
|
||
## 📝 참고사항
|
||
|
||
1. **멀티테넌시**: 모든 테이블에 `tenant_id` 필수, `BelongsToTenant` 스코프 적용
|
||
2. **감사 로그**: `created_by`, `updated_by`, `deleted_by` 컬럼 필수
|
||
3. **Soft Delete**: 모든 테이블에 `deleted_at` 적용
|
||
4. **i18n**: 모든 메시지는 `__('message.xxx')` 형식 사용
|
||
5. **Swagger**: 모든 엔드포인트에 문서 작성 필수
|
||
|
||
---
|
||
|
||
## 📊 최종 요약
|
||
|
||
### 🔑 핵심 정책
|
||
|
||
**JSON 필드 활용 원칙:**
|
||
- 인덱싱 필요 필드 → 컬럼으로 유지
|
||
- 확장/복잡한 필드 → JSON 컬럼에 저장
|
||
- 추후 인덱싱 필요시 → MySQL 가상 컬럼 + 인덱스
|
||
|
||
```
|
||
사용자(User)와 사원(Employee) 완전 분리
|
||
|
||
users 테이블 (전역 - 시스템 계정)
|
||
├── 계정: id, user_id, name, email, phone, password
|
||
├── 상태: is_active, is_super_admin, must_change_password
|
||
└── 인증: email_verified_at, last_login_at, two_factor_*
|
||
|
||
tenant_user_profiles 테이블 (테넌트별 - 사원 정보)
|
||
├── 컬럼: employee_status (인덱싱), department_id, position_key, employment_type_key
|
||
└── json_extra: employee_code, resident_number, gender, address,
|
||
salary, hire_date, rank, bank_account, work_type, contract_info
|
||
|
||
attendances 테이블 (근태 - json_details 활용)
|
||
├── 컬럼: base_date, status (인덱싱)
|
||
└── json_details: check_in, check_out, gps_data, external_work, multiple_entries
|
||
```
|
||
|
||
#### 상태 필드 구분
|
||
| 필드 | 의미 | 레벨 |
|
||
|------|------|------|
|
||
| `users.is_active` | 시스템 계정 활성화 | 전역 |
|
||
| `user_tenants.is_active` | 테넌트 멤버십 활성화 | 테넌트별 |
|
||
| `tenant_user_profiles.employee_status` | 고용 상태 (재직/휴직/퇴직) | 테넌트별 |
|
||
|
||
### 프로젝트별 현황
|
||
|
||
| 프로젝트 | 사원관리 | 부서관리 | 근태관리 | 비고 |
|
||
|---------|---------|---------|---------|------|
|
||
| **React** | ✅ UI 완성 (Mock) | ✅ UI 완성 (Mock) | ✅ UI 완성 (Mock) | API 연동 필요 |
|
||
| **API** | ❌ 없음 | ⚠️ 플랫 구조 | ❌ 없음 | 신규 개발 필요 |
|
||
| **MNG** | ✅ User (참조용) | ✅ 트리 구조 | ❌ 없음 | API 개발 참조 |
|
||
|
||
### 핵심 결론
|
||
|
||
1. **Employee API**: tenant_user_profiles.json_extra 활용
|
||
- 사용자(users) = 전역 시스템 계정
|
||
- 사원(tenant_user_profiles) = 테넌트별 인사 정보
|
||
- employee_status만 컬럼, 나머지는 json_extra에 JSON으로 저장
|
||
|
||
2. **Department API**: 기존 API + MNG 트리 구조 패턴 병합
|
||
- 추가: parent_id, depth, children 관계
|
||
|
||
3. **Attendance API**: json_details 활용
|
||
- 인덱싱 필요 필드(base_date, status)만 컬럼
|
||
- 나머지는 json_details에 JSON으로 저장
|
||
- GPS 자동 출퇴근, 외부 근무, 복수 출퇴근 지원
|
||
|
||
4. **개발 순서**: Phase 1(DB) → Phase 2(Employee) → Phase 3(Department 수정) → Phase 4(Attendance)
|
||
|
||
### 예상 작업량
|
||
|
||
| Phase | 내용 | 예상 일수 |
|
||
|-------|------|----------|
|
||
| Phase 1 | DB 스키마 (마이그레이션) | 0.5일 |
|
||
| Phase 2 | 사원관리 API (json_extra 활용) | 1.5일 |
|
||
| Phase 3 | 부서관리 API 수정 | 1일 |
|
||
| Phase 4 | 근태관리 API (json_details 활용) | 2일 |
|
||
| **합계** | | **5일** |
|
||
|
||
### 장점 요약
|
||
|
||
1. **완전한 분리**: 사용자(전역) vs 사원(테넌트별) 명확한 구분
|
||
2. **역할 분리**: users(계정) / user_tenants(멤버십) / tenant_user_profiles(사원 프로필)
|
||
3. **테넌트별 관리**: 같은 사람이 테넌트별로 다른 사원코드, 급여, 직급 가능
|
||
4. **JSON 활용**: 스키마 변경 없이 필드 추가 가능, 유연한 확장
|
||
5. **인덱싱 최적화**: 검색 필요 필드만 컬럼, 나머지 JSON으로 효율적 관리
|
||
6. **복잡한 시나리오 지원**: GPS 출퇴근, 다양한 사원 형태, 복수 출퇴근 등
|