Files
sam-api/app/Services/SalaryService.php
kent a1aa8726af feat: 공통 모듈 추가 (엑셀 내보내기, 계정과목 일괄변경)
Phase 0 - 공통 모듈:
- ExportService.php 생성 (Maatwebsite/Excel 기반 엑셀 내보내기)
- BulkUpdateAccountCodeRequest.php 생성 (계정과목 일괄변경 유효성 검사)

Phase 1 - 계정과목 일괄변경:
- WithdrawalController/Service: bulkUpdateAccountCode 메서드 추가
- DepositController/Service: bulkUpdateAccountCode 메서드 추가
- POST /v1/withdrawals/bulk-update-account-code
- POST /v1/deposits/bulk-update-account-code

Phase 2 - 엑셀 내보내기:
- AttendanceController/Service: export, getExportData 메서드 추가
- SalaryController/Service: export, getExportData 메서드 추가
- GET /v1/attendances/export
- GET /v1/salaries/export

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-15 17:14:04 +09:00

401 lines
13 KiB
PHP

<?php
namespace App\Services;
use App\Models\Tenants\Salary;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class SalaryService extends Service
{
/**
* 급여 목록 조회
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = Salary::query()
->where('tenant_id', $tenantId)
->with([
'employee:id,name,user_id,email',
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
'employeeProfile.department:id,name',
]);
// 검색 필터 (직원명)
if (! empty($params['search'])) {
$search = $params['search'];
$query->whereHas('employee', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
}
// 연도 필터
if (! empty($params['year'])) {
$query->where('year', $params['year']);
}
// 월 필터
if (! empty($params['month'])) {
$query->where('month', $params['month']);
}
// 상태 필터
if (! empty($params['status'])) {
$query->where('status', $params['status']);
}
// 기간 필터
if (! empty($params['start_date']) && ! empty($params['end_date'])) {
$query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]);
}
// 직원 ID 필터
if (! empty($params['employee_id'])) {
$query->where('employee_id', $params['employee_id']);
}
// 정렬
$sortBy = $params['sort_by'] ?? 'year';
$sortDir = $params['sort_dir'] ?? 'desc';
if ($sortBy === 'year') {
$query->orderBy('year', $sortDir)
->orderBy('month', $sortDir);
} else {
$query->orderBy($sortBy, $sortDir);
}
// 페이지네이션
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 급여 상세 조회
*/
public function show(int $id): Salary
{
$tenantId = $this->tenantId();
return Salary::query()
->where('tenant_id', $tenantId)
->with([
'employee:id,name,user_id,email',
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
'employeeProfile.department:id,name',
])
->findOrFail($id);
}
/**
* 급여 등록
*/
public function store(array $data): Salary
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
$salary = new Salary;
$salary->tenant_id = $tenantId;
$salary->employee_id = $data['employee_id'];
$salary->year = $data['year'];
$salary->month = $data['month'];
$salary->base_salary = $data['base_salary'] ?? 0;
$salary->total_allowance = $data['total_allowance'] ?? 0;
$salary->total_overtime = $data['total_overtime'] ?? 0;
$salary->total_bonus = $data['total_bonus'] ?? 0;
$salary->total_deduction = $data['total_deduction'] ?? 0;
$salary->allowance_details = $data['allowance_details'] ?? null;
$salary->deduction_details = $data['deduction_details'] ?? null;
$salary->payment_date = $data['payment_date'] ?? null;
$salary->status = $data['status'] ?? 'scheduled';
$salary->created_by = $userId;
$salary->updated_by = $userId;
// 실지급액 계산
$salary->net_payment = $salary->calculateNetPayment();
$salary->save();
return $salary->load('employee:id,name,email');
});
}
/**
* 급여 수정
*/
public function update(int $id, array $data): Salary
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$salary = Salary::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (isset($data['employee_id'])) {
$salary->employee_id = $data['employee_id'];
}
if (isset($data['year'])) {
$salary->year = $data['year'];
}
if (isset($data['month'])) {
$salary->month = $data['month'];
}
if (isset($data['base_salary'])) {
$salary->base_salary = $data['base_salary'];
}
if (isset($data['total_allowance'])) {
$salary->total_allowance = $data['total_allowance'];
}
if (isset($data['total_overtime'])) {
$salary->total_overtime = $data['total_overtime'];
}
if (isset($data['total_bonus'])) {
$salary->total_bonus = $data['total_bonus'];
}
if (isset($data['total_deduction'])) {
$salary->total_deduction = $data['total_deduction'];
}
if (array_key_exists('allowance_details', $data)) {
$salary->allowance_details = $data['allowance_details'];
}
if (array_key_exists('deduction_details', $data)) {
$salary->deduction_details = $data['deduction_details'];
}
if (array_key_exists('payment_date', $data)) {
$salary->payment_date = $data['payment_date'];
}
if (isset($data['status'])) {
$salary->status = $data['status'];
}
// 실지급액 재계산
$salary->net_payment = $salary->calculateNetPayment();
$salary->updated_by = $userId;
$salary->save();
return $salary->fresh()->load([
'employee:id,name,user_id,email',
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
'employeeProfile.department:id,name',
]);
});
}
/**
* 급여 삭제
*/
public function destroy(int $id): bool
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$salary = Salary::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$salary->deleted_by = $userId;
$salary->save();
$salary->delete();
return true;
});
}
/**
* 급여 상태 변경 (지급완료/지급예정)
*/
public function updateStatus(int $id, string $status): Salary
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $status, $tenantId, $userId) {
$salary = Salary::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$salary->status = $status;
$salary->updated_by = $userId;
$salary->save();
return $salary->load([
'employee:id,name,user_id,email',
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
'employeeProfile.department:id,name',
]);
});
}
/**
* 급여 일괄 상태 변경
*/
public function bulkUpdateStatus(array $ids, string $status): int
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($ids, $status, $tenantId, $userId) {
return Salary::query()
->where('tenant_id', $tenantId)
->whereIn('id', $ids)
->update([
'status' => $status,
'updated_by' => $userId,
'updated_at' => now(),
]);
});
}
/**
* 엑셀 내보내기용 데이터 조회
*
* @return array{data: array<int, array<string, mixed>>, headings: array<int, string>}
*/
public function getExportData(array $params): array
{
$tenantId = $this->tenantId();
$query = Salary::query()
->where('tenant_id', $tenantId)
->with([
'employee:id,name,user_id,email',
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
'employeeProfile.department:id,name',
]);
// 검색 필터 (직원명)
if (! empty($params['search'])) {
$search = $params['search'];
$query->whereHas('employee', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
}
// 연도 필터
if (! empty($params['year'])) {
$query->where('year', $params['year']);
}
// 월 필터
if (! empty($params['month'])) {
$query->where('month', $params['month']);
}
// 상태 필터
if (! empty($params['status'])) {
$query->where('status', $params['status']);
}
// 기간 필터
if (! empty($params['start_date']) && ! empty($params['end_date'])) {
$query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]);
}
// 직원 ID 필터
if (! empty($params['employee_id'])) {
$query->where('employee_id', $params['employee_id']);
}
// 정렬
$sortBy = $params['sort_by'] ?? 'year';
$sortDir = $params['sort_dir'] ?? 'desc';
if ($sortBy === 'year') {
$query->orderBy('year', $sortDir)
->orderBy('month', $sortDir);
} else {
$query->orderBy($sortBy, $sortDir);
}
$salaries = $query->get();
// 상태 레이블 매핑
$statusLabels = [
'scheduled' => '지급예정',
'completed' => '지급완료',
'pending' => '보류',
];
// 엑셀 데이터 변환
$data = $salaries->map(function ($salary) use ($statusLabels) {
return [
$salary->year.'년 '.$salary->month.'월',
$salary->employee?->name ?? '-',
$salary->employeeProfile?->department?->name ?? '-',
number_format($salary->base_salary),
number_format($salary->total_allowance),
number_format($salary->total_overtime),
number_format($salary->total_bonus),
number_format($salary->total_deduction),
number_format($salary->net_payment),
$statusLabels[$salary->status] ?? $salary->status,
$salary->payment_date ?? '-',
];
})->toArray();
$headings = [
'급여월',
'직원명',
'부서',
'기본급',
'수당',
'야근수당',
'상여금',
'공제액',
'실지급액',
'상태',
'지급일',
];
return [
'data' => $data,
'headings' => $headings,
];
}
/**
* 급여 통계 조회
*/
public function getStatistics(array $params): array
{
$tenantId = $this->tenantId();
$query = Salary::query()
->where('tenant_id', $tenantId);
// 연도/월 필터
if (! empty($params['year'])) {
$query->where('year', $params['year']);
}
if (! empty($params['month'])) {
$query->where('month', $params['month']);
}
// 기간 필터
if (! empty($params['start_date']) && ! empty($params['end_date'])) {
$query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]);
}
return [
'total_net_payment' => (float) $query->sum('net_payment'),
'total_base_salary' => (float) $query->sum('base_salary'),
'total_allowance' => (float) $query->sum('total_allowance'),
'total_overtime' => (float) $query->sum('total_overtime'),
'total_bonus' => (float) $query->sum('total_bonus'),
'total_deduction' => (float) $query->sum('total_deduction'),
'count' => $query->count(),
'completed_count' => (clone $query)->where('status', 'completed')->count(),
'scheduled_count' => (clone $query)->where('status', 'scheduled')->count(),
];
}
}