Files
sam-manage/app/Services/HR/BusinessIncomeEarnerService.php
김보곤 61a0cc2480 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:47:10 +09:00

395 lines
14 KiB
PHP

<?php
namespace App\Services\HR;
use App\Models\HR\BusinessIncomeEarner;
use App\Models\HR\Position;
use App\Models\Tenants\Department;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class BusinessIncomeEarnerService
{
/**
* 사업소득자 목록 조회 (페이지네이션)
*/
public function getBusinessIncomeEarners(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
$tenantId = session('selected_tenant_id');
$query = BusinessIncomeEarner::query()
->with(['user', 'department'])
->forTenant($tenantId);
if (! empty($filters['q'])) {
$search = $filters['q'];
$query->where(function ($q) use ($search) {
$q->where('display_name', 'like', "%{$search}%")
->orWhereHas('user', function ($uq) use ($search) {
$uq->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('phone', 'like', "%{$search}%");
});
});
}
if (! empty($filters['status'])) {
$query->where('employee_status', $filters['status']);
}
if (! empty($filters['department_id'])) {
$query->where('department_id', $filters['department_id']);
}
$sortBy = $filters['sort_by'] ?? 'hire_date_asc';
switch ($sortBy) {
case 'hire_date_asc':
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC");
break;
case 'hire_date_desc':
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC");
break;
case 'resign_date_asc':
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) ASC");
break;
case 'resign_date_desc':
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) DESC");
break;
default:
$query->orderByRaw("FIELD(employee_status, 'active', 'leave', 'resigned')")
->orderBy('created_at', 'desc');
break;
}
return $query->paginate($perPage);
}
/**
* 사업소득자 상세 조회
*/
public function getById(int $id): ?BusinessIncomeEarner
{
$tenantId = session('selected_tenant_id');
return BusinessIncomeEarner::query()
->with(['user', 'department', 'manager'])
->forTenant($tenantId)
->find($id);
}
/**
* 사업소득자 통계
*/
public function getStats(): array
{
$tenantId = session('selected_tenant_id');
$baseQuery = BusinessIncomeEarner::query()->forTenant($tenantId);
return [
'total' => (clone $baseQuery)->count(),
'active' => (clone $baseQuery)->where('employee_status', 'active')->count(),
'leave' => (clone $baseQuery)->where('employee_status', 'leave')->count(),
'resigned' => (clone $baseQuery)->where('employee_status', 'resigned')->count(),
];
}
/**
* 테넌트 소속이지만 사업소득자 미등록인 사용자 검색
*/
public function searchTenantUsers(string $query): array
{
$tenantId = session('selected_tenant_id');
$builder = User::query()
->select('users.id', 'users.name', 'users.email', 'users.phone')
->join('user_tenants as ut', function ($join) use ($tenantId) {
$join->on('users.id', '=', 'ut.user_id')
->where('ut.tenant_id', $tenantId)
->whereNull('ut.deleted_at');
})
->leftJoin('tenant_user_profiles as tup', function ($join) use ($tenantId) {
$join->on('users.id', '=', 'tup.user_id')
->where('tup.tenant_id', $tenantId);
})
->whereNull('tup.id')
->whereNull('users.deleted_at');
if ($query !== '') {
$like = "%{$query}%";
$builder->where(function ($q) use ($like) {
$q->where('users.name', 'like', $like)
->orWhere('users.email', 'like', $like)
->orWhere('users.phone', 'like', $like);
});
}
return $builder->orderBy('users.name')->limit(20)->get()->toArray();
}
/**
* 사업소득자 등록 (User is_active=false + TenantUserProfile 생성)
*/
public function create(array $data): BusinessIncomeEarner
{
$tenantId = session('selected_tenant_id');
return DB::transaction(function () use ($data, $tenantId) {
if (! empty($data['existing_user_id'])) {
$user = User::findOrFail($data['existing_user_id']);
$isMember = $user->tenants()
->wherePivot('tenant_id', $tenantId)
->wherePivotNull('deleted_at')
->exists();
if (! $isMember) {
throw new \RuntimeException('해당 사용자는 현재 테넌트에 소속되어 있지 않습니다.');
}
$alreadyRegistered = BusinessIncomeEarner::where('tenant_id', $tenantId)
->where('user_id', $user->id)
->exists();
if ($alreadyRegistered) {
throw new \RuntimeException('이미 사업소득자로 등록된 사용자입니다.');
}
} else {
// 신규 사용자 생성 (is_active=false, 로그인 불가)
$loginId = ! empty($data['email'])
? Str::before($data['email'], '@')
: 'BIZ_'.strtolower(Str::random(6));
while (User::where('user_id', $loginId)->exists()) {
$loginId = $loginId.'_'.Str::random(3);
}
$email = ! empty($data['email'])
? $data['email']
: $loginId.'@placeholder.local';
while (User::where('email', $email)->exists()) {
$email = $loginId.'_'.Str::random(3).'@placeholder.local';
}
$user = User::create([
'user_id' => $loginId,
'name' => $data['name'],
'email' => $email,
'phone' => $data['phone'] ?? null,
'password' => Hash::make(Str::random(32)),
'role' => 'ops',
'is_active' => false,
'must_change_password' => false,
'created_by' => auth()->id(),
]);
if ($tenantId) {
$user->tenants()->attach($tenantId, [
'is_active' => true,
'is_default' => true,
'joined_at' => now(),
]);
}
}
// json_extra 구성
$jsonExtra = [];
$scalarKeys = [
'hire_date', 'resign_date', 'address', 'emergency_contact', 'resident_number',
'business_registration_number', 'business_name', 'business_representative',
'business_type', 'business_category', 'business_address',
];
foreach ($scalarKeys as $key) {
if (! empty($data[$key])) {
$jsonExtra[$key] = $data[$key];
}
}
// 급여이체정보
if (! empty($data['bank_account']) && is_array($data['bank_account'])) {
$bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== '');
if (! empty($bankAccount)) {
$jsonExtra['bank_account'] = $bankAccount;
}
}
// 부양가족 정보
if (! empty($data['dependents']) && is_array($data['dependents'])) {
$dependents = array_values(array_filter($data['dependents'], function ($dep) {
return ! empty($dep['name']);
}));
$dependents = array_map(function ($dep) {
$dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
$dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN);
return $dep;
}, $dependents);
if (! empty($dependents)) {
$jsonExtra['dependents'] = $dependents;
}
}
$earner = BusinessIncomeEarner::create([
'tenant_id' => $tenantId,
'user_id' => $user->id,
'department_id' => $data['department_id'] ?? null,
'position_key' => $data['position_key'] ?? null,
'job_title_key' => $data['job_title_key'] ?? null,
'work_location_key' => $data['work_location_key'] ?? null,
'employment_type_key' => $data['employment_type_key'] ?? null,
'employee_status' => $data['employee_status'] ?? 'active',
'manager_user_id' => $data['manager_user_id'] ?? null,
'display_name' => $data['display_name'] ?? $data['name'],
'json_extra' => ! empty($jsonExtra) ? $jsonExtra : null,
]);
return $earner->load(['user', 'department']);
});
}
/**
* 사업소득자 정보 수정
*/
public function update(int $id, array $data): ?BusinessIncomeEarner
{
$earner = $this->getById($id);
if (! $earner) {
return null;
}
$updateData = array_filter([
'department_id' => $data['department_id'] ?? null,
'position_key' => $data['position_key'] ?? null,
'job_title_key' => $data['job_title_key'] ?? null,
'work_location_key' => $data['work_location_key'] ?? null,
'employment_type_key' => $data['employment_type_key'] ?? null,
'employee_status' => $data['employee_status'] ?? null,
'manager_user_id' => $data['manager_user_id'] ?? null,
'display_name' => $data['display_name'] ?? null,
], fn ($v) => $v !== null);
// json_extra 업데이트
$jsonExtraKeys = [
'hire_date', 'resign_date', 'address', 'emergency_contact', 'salary', 'resident_number',
'business_registration_number', 'business_name', 'business_representative',
'business_type', 'business_category', 'business_address',
];
$extra = $earner->json_extra ?? [];
foreach ($jsonExtraKeys as $key) {
if (array_key_exists($key, $data)) {
if ($data[$key] === null || $data[$key] === '') {
unset($extra[$key]);
} else {
$extra[$key] = $data[$key];
}
}
}
// 급여이체정보
if (array_key_exists('bank_account', $data)) {
if (! empty($data['bank_account']) && is_array($data['bank_account'])) {
$bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== '');
if (! empty($bankAccount)) {
$extra['bank_account'] = $bankAccount;
} else {
unset($extra['bank_account']);
}
} else {
unset($extra['bank_account']);
}
}
// 부양가족 정보
if (array_key_exists('dependents', $data)) {
if (! empty($data['dependents']) && is_array($data['dependents'])) {
$dependents = array_values(array_filter($data['dependents'], function ($dep) {
return ! empty($dep['name']);
}));
$dependents = array_map(function ($dep) {
$dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
$dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN);
return $dep;
}, $dependents);
if (! empty($dependents)) {
$extra['dependents'] = $dependents;
} else {
unset($extra['dependents']);
}
} else {
unset($extra['dependents']);
}
}
$updateData['json_extra'] = ! empty($extra) ? $extra : null;
$earner->update($updateData);
// User 기본정보 동기화
if ($earner->user) {
$userUpdate = [];
if (! empty($data['name'])) {
$userUpdate['name'] = $data['name'];
}
if (! empty($data['email'])) {
$userUpdate['email'] = $data['email'];
}
if (! empty($data['phone'])) {
$userUpdate['phone'] = $data['phone'];
}
if (! empty($userUpdate)) {
$earner->user->update($userUpdate);
}
}
return $earner->fresh(['user', 'department']);
}
/**
* 사업소득자 삭제 (퇴직 처리)
*/
public function delete(int $id): bool
{
$earner = $this->getById($id);
if (! $earner) {
return false;
}
$earner->update(['employee_status' => 'resigned']);
return true;
}
/**
* 부서 목록 (드롭다운용)
*/
public function getDepartments(): \Illuminate\Database\Eloquent\Collection
{
$tenantId = session('selected_tenant_id');
return Department::query()
->where('is_active', true)
->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
->orderBy('sort_order')
->orderBy('name')
->get(['id', 'name', 'code']);
}
/**
* 직급/직책 목록 (드롭다운용)
*/
public function getPositions(string $type = 'rank'): \Illuminate\Database\Eloquent\Collection
{
return Position::query()
->forTenant()
->where('type', $type)
->where('is_active', true)
->ordered()
->get(['id', 'key', 'name']);
}
}