'정규직', 'contract' => '계약직', 'daily' => '일용직', 'freelancer' => '프리랜서', ]; protected $table = 'tenant_user_profiles'; protected $fillable = [ 'tenant_id', 'user_id', 'department_id', 'position_key', 'job_title_key', 'work_location_key', 'employment_type_key', 'employee_status', 'worker_type', 'manager_user_id', 'json_extra', 'profile_photo_path', 'display_name', ]; protected static function booted(): void { static::addGlobalScope('employee', function (Builder $builder) { $builder->where(function ($q) { $q->where('worker_type', 'employee') ->orWhereNull('worker_type'); }); }); } protected $casts = [ 'json_extra' => 'array', 'tenant_id' => 'int', 'user_id' => 'int', 'department_id' => 'int', 'manager_user_id' => 'int', ]; protected $appends = [ 'hire_date', 'resign_date', 'position_label', 'job_title_label', ]; // ========================================================================= // 관계 정의 // ========================================================================= public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function department(): BelongsTo { return $this->belongsTo(Department::class, 'department_id'); } public function manager(): BelongsTo { return $this->belongsTo(User::class, 'manager_user_id'); } // ========================================================================= // json_extra Accessor // ========================================================================= public function getHireDateAttribute(): ?string { return $this->json_extra['hire_date'] ?? null; } public function getResignDateAttribute(): ?string { return $this->json_extra['resign_date'] ?? null; } public function getAddressAttribute(): ?string { return $this->json_extra['address'] ?? null; } public function getEmergencyContactAttribute(): ?string { return $this->json_extra['emergency_contact'] ?? null; } public function getResidentNumberAttribute(): ?string { return $this->json_extra['resident_number'] ?? null; } public function getPersonalEmailAttribute(): ?string { return $this->json_extra['personal_email'] ?? null; } public function getBankAccountAttribute(): ?array { return $this->json_extra['bank_account'] ?? null; } public function getDependentsAttribute(): array { return $this->json_extra['dependents'] ?? []; } public function getPositionLabelAttribute(): ?string { if (! $this->position_key || ! $this->tenant_id) { return $this->position_key; } $position = Position::where('tenant_id', $this->tenant_id) ->where('type', Position::TYPE_RANK) ->where('key', $this->position_key) ->first(); return $position?->name ?? $this->position_key; } public function getJobTitleLabelAttribute(): ?string { if (! $this->job_title_key || ! $this->tenant_id) { return $this->job_title_key; } $position = Position::where('tenant_id', $this->tenant_id) ->where('type', Position::TYPE_TITLE) ->where('key', $this->job_title_key) ->first(); return $position?->name ?? $this->job_title_key; } // ========================================================================= // json_extra 헬퍼 // ========================================================================= public function getJsonExtraValue(string $key, mixed $default = null): mixed { return $this->json_extra[$key] ?? $default; } public function setJsonExtraValue(string $key, mixed $value): void { $extra = $this->json_extra ?? []; if ($value === null) { unset($extra[$key]); } else { $extra[$key] = $value; } $this->json_extra = $extra; } // ========================================================================= // 연봉 정보 (salary_info) — 민감 데이터, 별도 접근 제어 // ========================================================================= public function getSalaryInfo(): array { $defaults = [ 'annual_salary' => null, 'effective_date' => null, 'notes' => null, 'fixed_overtime_hours' => null, 'meal_allowance' => 200000, 'monthly_salary' => null, 'base_salary' => null, 'fixed_overtime_pay' => null, 'hourly_wage' => null, 'monthly_work_hours' => 209, 'overtime_multiplier' => 1.5, 'history' => [], ]; $data = $this->json_extra['salary_info'] ?? []; return array_merge($defaults, $data); } public function setSalaryInfo(array $data): void { $current = $this->getSalaryInfo(); $history = $current['history'] ?? []; // 기존 연봉이 있으면 이력에 추가 if ($current['annual_salary'] !== null) { $history[] = [ 'annual_salary' => $current['annual_salary'], 'fixed_overtime_hours' => $current['fixed_overtime_hours'], 'meal_allowance' => $current['meal_allowance'], 'base_salary' => $current['base_salary'], 'fixed_overtime_pay' => $current['fixed_overtime_pay'], 'effective_date' => $current['effective_date'], 'notes' => $current['notes'], 'recorded_at' => now()->format('Y-m-d H:i:s'), 'recorded_by' => auth()->user()?->name ?? '-', ]; } $annualSalary = $data['annual_salary'] ?? null; $mealAllowance = $data['meal_allowance'] ?? 200000; $fixedOvertimeHours = $data['fixed_overtime_hours'] ?? null; $breakdown = $this->calculateSalaryBreakdown($annualSalary, $mealAllowance, $fixedOvertimeHours); $this->setJsonExtraValue('salary_info', array_merge([ 'annual_salary' => $annualSalary, 'effective_date' => $data['effective_date'] ?? null, 'notes' => $data['notes'] ?? null, 'fixed_overtime_hours' => $fixedOvertimeHours, 'meal_allowance' => $mealAllowance, ], $breakdown, ['history' => $history])); } /** * 급여 산정 계산 * * 공식: (기본급 + 식대) = 월급여 × 209 / (209 + 고정연장근로시간 × 1.5) */ private function calculateSalaryBreakdown(?int $annualSalary, int $mealAllowance, ?int $fixedOvertimeHours): array { $monthlyWorkHours = 209; $overtimeMultiplier = 1.5; if (! $annualSalary) { return [ 'monthly_salary' => null, 'base_salary' => null, 'fixed_overtime_pay' => null, 'hourly_wage' => null, 'monthly_work_hours' => $monthlyWorkHours, 'overtime_multiplier' => $overtimeMultiplier, ]; } $monthlySalary = (int) round($annualSalary / 12); $otFactor = ($fixedOvertimeHours ?? 0) * $overtimeMultiplier; $basePlusMeal = (int) round($monthlySalary * $monthlyWorkHours / ($monthlyWorkHours + $otFactor)); $baseSalary = $basePlusMeal - $mealAllowance; $hourlyWage = (int) floor($basePlusMeal / $monthlyWorkHours); $fixedOvertimePay = $monthlySalary - $baseSalary - $mealAllowance; return [ 'monthly_salary' => $monthlySalary, 'base_salary' => $baseSalary, 'fixed_overtime_pay' => $fixedOvertimePay, 'hourly_wage' => $hourlyWage, 'monthly_work_hours' => $monthlyWorkHours, 'overtime_multiplier' => $overtimeMultiplier, ]; } /** * toArray 시 salary_info 제거 (일반 API 응답에서 연봉 정보 노출 방지) */ public function toArray(): array { $array = parent::toArray(); if (isset($array['json_extra']['salary_info'])) { unset($array['json_extra']['salary_info']); } return $array; } // ========================================================================= // 스코프 // ========================================================================= public function scopeForTenant($query, ?int $tenantId = null) { $tenantId = $tenantId ?? session('selected_tenant_id'); if ($tenantId) { return $query->where('tenant_id', $tenantId); } return $query; } public function scopeActiveEmployees($query) { return $query->where('employee_status', 'active'); } public function scopeOnLeave($query) { return $query->where('employee_status', 'leave'); } public function scopeResigned($query) { return $query->where('employee_status', 'resigned'); } }