Files
sam-docs/features/hr/hr-api-analysis.md
hskwon 73b8b9325b docs: HR API 분석 문서 재구성
- features/hr/hr-api-analysis.md 추가 (상세 분석)
- features/HR_API_ANALYSIS.md 삭제 (hr 폴더로 이동)
2025-12-09 20:30:33 +09:00

1004 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 출퇴근, 다양한 사원 형태, 복수 출퇴근 등