diff --git a/app/Http/Controllers/Api/Admin/HR/PayrollController.php b/app/Http/Controllers/Api/Admin/HR/PayrollController.php index 98990a71..927f0dff 100644 --- a/app/Http/Controllers/Api/Admin/HR/PayrollController.php +++ b/app/Http/Controllers/Api/Admin/HR/PayrollController.php @@ -406,13 +406,19 @@ public function calculate(Request $request): JsonResponse 'bonus' => 'nullable|numeric|min:0', 'allowances' => 'nullable|array', 'deductions' => 'nullable|array', + 'user_id' => 'nullable|integer', ]); - $result = $this->payrollService->calculateAmounts($validated); + $familyCount = 1; + if (! empty($validated['user_id'])) { + $familyCount = $this->payrollService->resolveFamilyCount($validated['user_id']); + } + + $result = $this->payrollService->calculateAmounts($validated, null, $familyCount); return response()->json([ 'success' => true, - 'data' => $result, + 'data' => array_merge($result, ['family_count' => $familyCount]), ]); } } diff --git a/app/Models/HR/IncomeTaxBracket.php b/app/Models/HR/IncomeTaxBracket.php new file mode 100644 index 00000000..76c9e62d --- /dev/null +++ b/app/Models/HR/IncomeTaxBracket.php @@ -0,0 +1,69 @@ + 'integer', + 'salary_from' => 'integer', + 'salary_to' => 'integer', + 'family_count' => 'integer', + 'tax_amount' => 'integer', + ]; + + public function scopeForYear(Builder $query, int $year): Builder + { + return $query->where('tax_year', $year); + } + + public function scopeForSalaryRange(Builder $query, int $salaryThousand): Builder + { + return $query->where('salary_from', '<=', $salaryThousand) + ->where(function ($q) use ($salaryThousand) { + $q->where('salary_to', '>', $salaryThousand) + ->orWhere(function ($q2) use ($salaryThousand) { + // 10,000천원 정확값 (salary_from == salary_to == 10000) + $q2->whereColumn('salary_from', 'salary_to') + ->where('salary_from', $salaryThousand); + }); + }); + } + + public function scopeForFamilyCount(Builder $query, int $count): Builder + { + return $query->where('family_count', $count); + } + + /** + * 간이세액표에서 세액 조회 + * + * @param int $year 세액표 적용 연도 + * @param int $salaryThousand 월급여 (천원 단위) + * @param int $familyCount 공제대상가족수 (1~11) + */ + public static function lookupTax(int $year, int $salaryThousand, int $familyCount): int + { + $familyCount = max(1, min(11, $familyCount)); + + $bracket = static::forYear($year) + ->forSalaryRange($salaryThousand) + ->forFamilyCount($familyCount) + ->first(); + + return $bracket ? $bracket->tax_amount : 0; + } +} diff --git a/app/Services/HR/PayrollService.php b/app/Services/HR/PayrollService.php index c0599a4e..58cfc28a 100644 --- a/app/Services/HR/PayrollService.php +++ b/app/Services/HR/PayrollService.php @@ -3,6 +3,7 @@ namespace App\Services\HR; use App\Models\HR\Employee; +use App\Models\HR\IncomeTaxBracket; use App\Models\HR\Payroll; use App\Models\HR\PayrollSetting; use App\Models\Tenants\Department; @@ -12,34 +13,7 @@ class PayrollService { - // ========================================================================= - // 간이세액표 (국세청 근로소득 간이세액표 기준, 부양가족 1인) - // 월 급여액 구간별 세액 (원) - // ========================================================================= - - private const INCOME_TAX_TABLE = [ - // [하한, 상한, 세액] - [0, 1060000, 0], - [1060000, 1500000, 19060], - [1500000, 2000000, 34680], - [2000000, 2500000, 60430], - [2500000, 3000000, 95960], - [3000000, 3500000, 137900], - [3500000, 4000000, 179000], - [4000000, 4500000, 215060], - [4500000, 5000000, 258190], - [5000000, 6000000, 311810], - [6000000, 7000000, 414810], - [7000000, 8000000, 519370], - [8000000, 9000000, 633640], - [9000000, 10000000, 758640], - [10000000, 14000000, 974780], - [14000000, 28000000, 1801980], - [28000000, 30000000, 5765680], - [30000000, 45000000, 6387680], - [45000000, 87000000, 11462680], - [87000000, PHP_INT_MAX, 29082680], - ]; + private const TAX_TABLE_YEAR = 2024; /** * 필터 적용 쿼리 생성 (목록/엑셀 공통) @@ -155,7 +129,8 @@ public function storePayroll(array $data): Payroll } return DB::transaction(function () use ($data, $tenantId) { - $calculated = $this->calculateAmounts($data); + $familyCount = $data['family_count'] ?? $this->resolveFamilyCount($data['user_id']); + $calculated = $this->calculateAmounts($data, null, $familyCount); $this->applyDeductionOverrides($calculated, $data['deduction_overrides'] ?? null); $payroll = Payroll::create([ @@ -203,7 +178,8 @@ public function updatePayroll(int $id, array $data): ?Payroll } $mergedData = array_merge($payroll->toArray(), $data); - $calculated = $this->calculateAmounts($mergedData); + $familyCount = $data['family_count'] ?? $this->resolveFamilyCount($payroll->user_id); + $calculated = $this->calculateAmounts($mergedData, null, $familyCount); $this->applyDeductionOverrides($calculated, $data['deduction_overrides'] ?? null); $payroll->update([ @@ -339,7 +315,11 @@ public function bulkGenerate(int $year, int $month): array 'deductions' => null, ]; - $calculated = $this->calculateAmounts($data, $settings); + // 본인(1) + is_dependent=true인 피부양자 수 + $familyCount = 1 + collect($employee->dependents) + ->where('is_dependent', true)->count(); + + $calculated = $this->calculateAmounts($data, $settings, $familyCount); Payroll::create([ 'tenant_id' => $tenantId, @@ -378,7 +358,7 @@ public function bulkGenerate(int $year, int $month): array * 식대(bonus)는 비과세 항목으로, 총 지급액에는 포함되지만 * 4대보험 및 세금 산출 기준(과세표준)에서는 제외된다. */ - public function calculateAmounts(array $data, ?PayrollSetting $settings = null): array + public function calculateAmounts(array $data, ?PayrollSetting $settings = null, int $familyCount = 1): array { $settings = $settings ?? PayrollSetting::getOrCreate(); @@ -407,10 +387,10 @@ public function calculateAmounts(array $data, ?PayrollSetting $settings = null): $pension = $this->calculatePension($taxableBase, $settings); $employmentInsurance = $this->calculateEmploymentInsurance($taxableBase, $settings); - // 근로소득세 (간이세액표, 과세표준 기준) - $incomeTax = $this->calculateIncomeTax($taxableBase); - // 지방소득세 (근로소득세의 10%) - $residentTax = (int) floor($incomeTax * ($settings->resident_tax_rate / 100)); + // 근로소득세 (간이세액표, 과세표준 기준, 공제대상가족수 반영) + $incomeTax = $this->calculateIncomeTax($taxableBase, $familyCount); + // 지방소득세 (근로소득세의 10%, 10원 단위 절삭) + $residentTax = (int) (floor($incomeTax * ($settings->resident_tax_rate / 100) / 10) * 10); // 추가 공제 합계 $extraDeductions = 0; @@ -471,28 +451,86 @@ private function applyDeductionOverrides(array &$calculated, ?array $overrides): } /** - * 근로소득세 계산 (간이세액표 기준, 부양가족 1인) + * 근로소득세 계산 (2024 국세청 간이세액표 기반) + * + * @param float $taxableBase 과세표준 (원) + * @param int $familyCount 공제대상가족수 (1~11, 본인 포함) */ - public function calculateIncomeTax(float $grossSalary, int $dependents = 1): int + public function calculateIncomeTax(float $taxableBase, int $familyCount = 1): int { - if ($grossSalary <= 0) { + if ($taxableBase <= 0) { return 0; } - $tax = 0; - foreach (self::INCOME_TAX_TABLE as [$min, $max, $amount]) { - if ($grossSalary > $min && $grossSalary <= $max) { - $tax = $amount; - break; - } + $salaryThousand = (int) floor($taxableBase / 1000); + $familyCount = max(1, min(11, $familyCount)); + + // 770천원 미만: 세액 없음 + if ($salaryThousand < 770) { + return 0; } - // 마지막 구간 초과 - if ($grossSalary > 87000000) { - $tax = self::INCOME_TAX_TABLE[count(self::INCOME_TAX_TABLE) - 1][2]; + // 10,000천원 초과: 공식 계산 + if ($salaryThousand > 10000) { + return $this->calculateHighIncomeTax($salaryThousand, $familyCount); } - return (int) $tax; + // DB 조회 (간이세액표) + return IncomeTaxBracket::lookupTax(self::TAX_TABLE_YEAR, $salaryThousand, $familyCount); + } + + /** + * 10,000천원 초과 구간 근로소득세 공식 계산 + * + * 소득세법 시행령 별표2 기준: + * - 10,000~14,000천원: (1천만원 세액) + (초과분 × 98% × 35%) + 25,000 + * - 14,000~28,000천원: (1천만원 세액) + 1,397,000 + (초과분 × 98% × 38%) + * - 28,000~30,000천원: (1천만원 세액) + 6,610,600 + (초과분 × 98% × 40%) + * - 30,000~45,000천원: (1천만원 세액) + 7,394,600 + (초과분 × 40%) + * - 45,000~87,000천원: (1천만원 세액) + 13,394,600 + (초과분 × 42%) + * - 87,000천원 초과: (1천만원 세액) + 31,034,600 + (초과분 × 45%) + */ + private function calculateHighIncomeTax(int $salaryThousand, int $familyCount): int + { + // 1천만원(10,000천원) 정확값의 가족수별 세액 (DB에서 조회) + $baseTax = IncomeTaxBracket::where('tax_year', self::TAX_TABLE_YEAR) + ->where('salary_from', 10000) + ->whereColumn('salary_from', 'salary_to') + ->where('family_count', $familyCount) + ->value('tax_amount') ?? 0; + + // 초과분 계산 (천원 단위 → 원 단위) + $excessThousand = $salaryThousand - 10000; + $excessWon = $excessThousand * 1000; + + if ($salaryThousand <= 14000) { + return (int) ($baseTax + ($excessWon * 0.98 * 0.35) + 25000); + } + if ($salaryThousand <= 28000) { + $excessWon = ($salaryThousand - 14000) * 1000; + + return (int) ($baseTax + 1397000 + ($excessWon * 0.98 * 0.38)); + } + if ($salaryThousand <= 30000) { + $excessWon = ($salaryThousand - 28000) * 1000; + + return (int) ($baseTax + 6610600 + ($excessWon * 0.98 * 0.40)); + } + if ($salaryThousand <= 45000) { + $excessWon = ($salaryThousand - 30000) * 1000; + + return (int) ($baseTax + 7394600 + ($excessWon * 0.40)); + } + if ($salaryThousand <= 87000) { + $excessWon = ($salaryThousand - 45000) * 1000; + + return (int) ($baseTax + 13394600 + ($excessWon * 0.42)); + } + + // 87,000천원 초과 + $excessWon = ($salaryThousand - 87000) * 1000; + + return (int) ($baseTax + 31034600 + ($excessWon * 0.45)); } /** @@ -569,6 +607,29 @@ public function getSettings(): PayrollSetting return PayrollSetting::getOrCreate(); } + /** + * user_id로 공제대상가족수 산출 (본인 1 + is_dependent 피부양자) + */ + public function resolveFamilyCount(int $userId): int + { + $tenantId = session('selected_tenant_id', 1); + + $employee = Employee::query() + ->forTenant($tenantId) + ->where('user_id', $userId) + ->first(['json_extra']); + + if (! $employee) { + return 1; + } + + $dependentCount = collect($employee->dependents) + ->where('is_dependent', true) + ->count(); + + return max(1, min(11, 1 + $dependentCount)); + } + /** * 급여 설정 수정 */ diff --git a/resources/views/hr/payrolls/index.blade.php b/resources/views/hr/payrolls/index.blade.php index 4d931147..e76289ff 100644 --- a/resources/views/hr/payrolls/index.blade.php +++ b/resources/views/hr/payrolls/index.blade.php @@ -155,7 +155,8 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 f @foreach($employees as $emp) @endforeach @@ -282,6 +283,10 @@ class="money-input w-28 px-2 py-1 border border-gray-200 rounded text-right text 과세표준 (식대 제외) 0 +
+ 공제대상가족수 + 1명 +
총 공제액 0 @@ -500,10 +505,14 @@ function closePayrollModal() { document.getElementById('payrollUserId').disabled = false; } - // ===== 사원 선택 시 기본급 자동 입력 ===== + // ===== 사원 선택 시 기본급 자동 입력 + 가족수 표시 ===== document.getElementById('payrollUserId').addEventListener('change', function() { - if (editingPayrollId) return; const selected = this.options[this.selectedIndex]; + const dependents = parseInt(selected.dataset.dependents || 1); + const fcEl = document.getElementById('calcFamilyCount'); + if (fcEl) fcEl.textContent = dependents + '명'; + + if (editingPayrollId) return; const salary = parseInt(selected.dataset.salary || 0); if (salary > 0) { setMoneyValue(document.getElementById('payrollBaseSalary'), Math.round(salary / 12)); @@ -570,6 +579,7 @@ function doRecalculate() { bonus: parseMoneyValue(document.getElementById('payrollBonus')), allowances: allowances, deductions: deductions, + user_id: parseInt(document.getElementById('payrollUserId').value) || null, }; fetch('{{ route("api.admin.hr.payrolls.calculate") }}', { @@ -599,6 +609,10 @@ function doRecalculate() { }); document.getElementById('calcGross').textContent = numberFormat(d.gross_salary); document.getElementById('calcTaxableBase').textContent = numberFormat(d.taxable_base); + if (d.family_count) { + const fcEl = document.getElementById('calcFamilyCount'); + if (fcEl) fcEl.textContent = d.family_count + '명'; + } updateDeductionTotals(); } })