2026-02-26 16:43:52 +09:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Models\HR;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Models\Tenants\Department;
|
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
|
use App\Traits\ModelTrait;
|
feat: [hr] 사업소득자관리 메뉴 신설
- BusinessIncomeEarner 모델 생성 (worker_type 글로벌 스코프)
- Employee 모델에 worker_type 글로벌 스코프 추가 (기존 사원 격리)
- BusinessIncomeEarnerService 생성 (등록/수정/삭제/조회)
- Web/API 컨트롤러 생성 (CRUD + 파일 업로드)
- 라우트 추가 (web.php, api.php)
- View 5개 생성 (index, create, show, edit, partials/table)
- 사업장등록정보 6개 필드 (사업자등록번호, 상호, 대표자명, 업태, 종목, 소재지)
2026-02-27 13:46:50 +09:00
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
2026-02-26 16:43:52 +09:00
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
|
|
|
|
|
|
|
class Employee extends Model
|
|
|
|
|
|
{
|
|
|
|
|
|
use ModelTrait;
|
|
|
|
|
|
|
2026-02-27 16:00:19 +09:00
|
|
|
|
const EMPLOYMENT_TYPES = [
|
|
|
|
|
|
'regular' => '정규직',
|
|
|
|
|
|
'contract' => '계약직',
|
|
|
|
|
|
'daily' => '일용직',
|
|
|
|
|
|
'freelancer' => '프리랜서',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-02-26 16:43:52 +09:00
|
|
|
|
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',
|
feat: [hr] 사업소득자관리 메뉴 신설
- BusinessIncomeEarner 모델 생성 (worker_type 글로벌 스코프)
- Employee 모델에 worker_type 글로벌 스코프 추가 (기존 사원 격리)
- BusinessIncomeEarnerService 생성 (등록/수정/삭제/조회)
- Web/API 컨트롤러 생성 (CRUD + 파일 업로드)
- 라우트 추가 (web.php, api.php)
- View 5개 생성 (index, create, show, edit, partials/table)
- 사업장등록정보 6개 필드 (사업자등록번호, 상호, 대표자명, 업태, 종목, 소재지)
2026-02-27 13:46:50 +09:00
|
|
|
|
'worker_type',
|
2026-02-26 16:43:52 +09:00
|
|
|
|
'manager_user_id',
|
|
|
|
|
|
'json_extra',
|
|
|
|
|
|
'profile_photo_path',
|
|
|
|
|
|
'display_name',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
feat: [hr] 사업소득자관리 메뉴 신설
- BusinessIncomeEarner 모델 생성 (worker_type 글로벌 스코프)
- Employee 모델에 worker_type 글로벌 스코프 추가 (기존 사원 격리)
- BusinessIncomeEarnerService 생성 (등록/수정/삭제/조회)
- Web/API 컨트롤러 생성 (CRUD + 파일 업로드)
- 라우트 추가 (web.php, api.php)
- View 5개 생성 (index, create, show, edit, partials/table)
- 사업장등록정보 6개 필드 (사업자등록번호, 상호, 대표자명, 업태, 종목, 소재지)
2026-02-27 13:46:50 +09:00
|
|
|
|
protected static function booted(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
static::addGlobalScope('employee', function (Builder $builder) {
|
|
|
|
|
|
$builder->where(function ($q) {
|
|
|
|
|
|
$q->where('worker_type', 'employee')
|
|
|
|
|
|
->orWhereNull('worker_type');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 16:43:52 +09:00
|
|
|
|
protected $casts = [
|
|
|
|
|
|
'json_extra' => 'array',
|
|
|
|
|
|
'tenant_id' => 'int',
|
|
|
|
|
|
'user_id' => 'int',
|
|
|
|
|
|
'department_id' => 'int',
|
|
|
|
|
|
'manager_user_id' => 'int',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
protected $appends = [
|
|
|
|
|
|
'hire_date',
|
2026-02-26 18:59:15 +09:00
|
|
|
|
'resign_date',
|
2026-02-26 16:43:52 +09:00
|
|
|
|
'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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 18:59:15 +09:00
|
|
|
|
public function getResignDateAttribute(): ?string
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->json_extra['resign_date'] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 16:43:52 +09:00
|
|
|
|
public function getAddressAttribute(): ?string
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->json_extra['address'] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function getEmergencyContactAttribute(): ?string
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->json_extra['emergency_contact'] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 19:59:15 +09:00
|
|
|
|
public function getResidentNumberAttribute(): ?string
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->json_extra['resident_number'] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 10:04:24 +09:00
|
|
|
|
public function getPersonalEmailAttribute(): ?string
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->json_extra['personal_email'] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 19:59:15 +09:00
|
|
|
|
public function getBankAccountAttribute(): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->json_extra['bank_account'] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function getDependentsAttribute(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->json_extra['dependents'] ?? [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 16:43:52 +09:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 16:27:49 +09:00
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// 연봉 정보 (salary_info) — 민감 데이터, 별도 접근 제어
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
public function getSalaryInfo(): array
|
|
|
|
|
|
{
|
2026-03-12 14:34:21 +09:00
|
|
|
|
$defaults = [
|
2026-03-11 16:27:49 +09:00
|
|
|
|
'annual_salary' => null,
|
|
|
|
|
|
'effective_date' => null,
|
|
|
|
|
|
'notes' => null,
|
2026-03-12 14:34:21 +09:00
|
|
|
|
'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,
|
2026-03-11 16:27:49 +09:00
|
|
|
|
'history' => [],
|
|
|
|
|
|
];
|
2026-03-12 14:34:21 +09:00
|
|
|
|
|
|
|
|
|
|
$data = $this->json_extra['salary_info'] ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
return array_merge($defaults, $data);
|
2026-03-11 16:27:49 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function setSalaryInfo(array $data): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$current = $this->getSalaryInfo();
|
|
|
|
|
|
$history = $current['history'] ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 연봉이 있으면 이력에 추가
|
|
|
|
|
|
if ($current['annual_salary'] !== null) {
|
|
|
|
|
|
$history[] = [
|
|
|
|
|
|
'annual_salary' => $current['annual_salary'],
|
2026-03-12 14:34:21 +09:00
|
|
|
|
'fixed_overtime_hours' => $current['fixed_overtime_hours'],
|
|
|
|
|
|
'meal_allowance' => $current['meal_allowance'],
|
|
|
|
|
|
'base_salary' => $current['base_salary'],
|
|
|
|
|
|
'fixed_overtime_pay' => $current['fixed_overtime_pay'],
|
2026-03-11 16:27:49 +09:00
|
|
|
|
'effective_date' => $current['effective_date'],
|
|
|
|
|
|
'notes' => $current['notes'],
|
|
|
|
|
|
'recorded_at' => now()->format('Y-m-d H:i:s'),
|
|
|
|
|
|
'recorded_by' => auth()->user()?->name ?? '-',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:34:21 +09:00
|
|
|
|
$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,
|
2026-03-11 16:27:49 +09:00
|
|
|
|
'effective_date' => $data['effective_date'] ?? null,
|
|
|
|
|
|
'notes' => $data['notes'] ?? null,
|
2026-03-12 14:34:21 +09:00
|
|
|
|
'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,
|
|
|
|
|
|
];
|
2026-03-11 16:27:49 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 16:43:52 +09:00
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// 스코프
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|