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