diff --git a/features/HR_API_ANALYSIS.md b/features/HR_API_ANALYSIS.md new file mode 100644 index 0000000..5916f08 --- /dev/null +++ b/features/HR_API_ANALYSIS.md @@ -0,0 +1,1003 @@ +# 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 출퇴근, 다양한 사원 형태, 복수 출퇴근 등