From 57b58a2297a7c09119f27458536ad4d75eebcddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 27 Feb 2026 17:30:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[payroll]=20=EC=A0=84=EC=9B=94=20?= =?UTF-8?q?=EA=B8=89=EC=97=AC=20=EB=B3=B5=EC=82=AC=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PayrollService에 copyFromPreviousMonth() 메서드 추가 - PayrollController에 copyFromPrevious() 액션 추가 - 전월 지급/공제 금액을 그대로 복사 (요율 재계산 없음) - 이미 존재하는 사원/연월은 스킵 처리 --- .../Api/Admin/HR/PayrollController.php | 39 +++++++++++ app/Services/HR/PayrollService.php | 68 +++++++++++++++++++ resources/views/hr/payrolls/index.blade.php | 39 +++++++++++ routes/api.php | 1 + 4 files changed, 147 insertions(+) diff --git a/app/Http/Controllers/Api/Admin/HR/PayrollController.php b/app/Http/Controllers/Api/Admin/HR/PayrollController.php index dfdbc267..5d9a7e03 100644 --- a/app/Http/Controllers/Api/Admin/HR/PayrollController.php +++ b/app/Http/Controllers/Api/Admin/HR/PayrollController.php @@ -271,6 +271,45 @@ public function pay(Request $request, int $id): JsonResponse } } + /** + * 전월 급여 복사 등록 + */ + public function copyFromPrevious(Request $request): JsonResponse + { + $validated = $request->validate([ + 'pay_year' => 'required|integer|min:2020|max:2100', + 'pay_month' => 'required|integer|min:1|max:12', + ]); + + try { + $result = $this->payrollService->copyFromPreviousMonth($validated['pay_year'], $validated['pay_month']); + + if (! empty($result['no_previous'])) { + $prevYear = $validated['pay_month'] === 1 ? $validated['pay_year'] - 1 : $validated['pay_year']; + $prevMonth = $validated['pay_month'] === 1 ? 12 : $validated['pay_month'] - 1; + + return response()->json([ + 'success' => false, + 'message' => "{$prevYear}년 {$prevMonth}월 급여 데이터가 없습니다.", + ], 422); + } + + return response()->json([ + 'success' => true, + 'message' => "전월 복사 완료: {$result['created']}건 생성, {$result['skipped']}건 건너뜀 (이미 존재).", + 'data' => $result, + ]); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => '전월 복사 중 오류가 발생했습니다.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + /** * 일괄 생성 */ diff --git a/app/Services/HR/PayrollService.php b/app/Services/HR/PayrollService.php index 8bdec39f..1fc5b300 100644 --- a/app/Services/HR/PayrollService.php +++ b/app/Services/HR/PayrollService.php @@ -302,6 +302,74 @@ public function payPayroll(int $id): ?Payroll return $payroll->fresh(['user']); } + /** + * 전월 급여 복사 등록 + */ + public function copyFromPreviousMonth(int $year, int $month): array + { + $tenantId = session('selected_tenant_id', 1); + + // 전월 계산 (1월 → 전년 12월) + $prevYear = $month === 1 ? $year - 1 : $year; + $prevMonth = $month === 1 ? 12 : $month - 1; + + $previousPayrolls = Payroll::query() + ->forTenant($tenantId) + ->forPeriod($prevYear, $prevMonth) + ->get(); + + if ($previousPayrolls->isEmpty()) { + return ['created' => 0, 'skipped' => 0, 'no_previous' => true]; + } + + $created = 0; + $skipped = 0; + + DB::transaction(function () use ($previousPayrolls, $tenantId, $year, $month, &$created, &$skipped) { + foreach ($previousPayrolls as $prev) { + $exists = Payroll::query() + ->where('tenant_id', $tenantId) + ->where('user_id', $prev->user_id) + ->forPeriod($year, $month) + ->exists(); + + if ($exists) { + $skipped++; + + continue; + } + + Payroll::create([ + 'tenant_id' => $tenantId, + 'user_id' => $prev->user_id, + 'pay_year' => $year, + 'pay_month' => $month, + 'base_salary' => $prev->base_salary, + 'overtime_pay' => $prev->overtime_pay, + 'bonus' => $prev->bonus, + 'allowances' => $prev->allowances, + 'gross_salary' => $prev->gross_salary, + 'income_tax' => $prev->income_tax, + 'resident_tax' => $prev->resident_tax, + 'health_insurance' => $prev->health_insurance, + 'long_term_care' => $prev->long_term_care, + 'pension' => $prev->pension, + 'employment_insurance' => $prev->employment_insurance, + 'deductions' => $prev->deductions, + 'total_deductions' => $prev->total_deductions, + 'net_salary' => $prev->net_salary, + 'status' => 'draft', + 'created_by' => auth()->id(), + 'updated_by' => auth()->id(), + ]); + + $created++; + } + }); + + return ['created' => $created, 'skipped' => $skipped]; + } + /** * 일괄 생성 (재직 사원 전체) */ diff --git a/resources/views/hr/payrolls/index.blade.php b/resources/views/hr/payrolls/index.blade.php index a4c14a75..6fb3f9ab 100644 --- a/resources/views/hr/payrolls/index.blade.php +++ b/resources/views/hr/payrolls/index.blade.php @@ -36,6 +36,13 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-70 일괄 생성 +