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