diff --git a/app/Http/Controllers/Api/Admin/HR/PayrollController.php b/app/Http/Controllers/Api/Admin/HR/PayrollController.php index 9522c7d2..061d207d 100644 --- a/app/Http/Controllers/Api/Admin/HR/PayrollController.php +++ b/app/Http/Controllers/Api/Admin/HR/PayrollController.php @@ -103,6 +103,11 @@ public function store(Request $request): JsonResponse 'success' => false, 'message' => $e->getMessage(), ], 422); + } catch (\Illuminate\Database\UniqueConstraintViolationException $e) { + return response()->json([ + 'success' => false, + 'message' => '해당 직원의 동일 기간 급여가 이미 등록되어 있습니다.', + ], 422); } catch (\Throwable $e) { report($e); diff --git a/app/Services/HR/PayrollService.php b/app/Services/HR/PayrollService.php index 8d787e5a..bdc09621 100644 --- a/app/Services/HR/PayrollService.php +++ b/app/Services/HR/PayrollService.php @@ -110,25 +110,25 @@ public function storePayroll(array $data): Payroll { $tenantId = session('selected_tenant_id', 1); - // 동일 대상/기간 중복 체크 (soft-deleted 포함 — DB 유니크 제약과 일치) - $existing = Payroll::withTrashed() - ->where('tenant_id', $tenantId) - ->where('user_id', $data['user_id']) - ->where('pay_year', $data['pay_year']) - ->where('pay_month', $data['pay_month']) - ->first(); - - if ($existing) { - if ($existing->trashed()) { - $existing->forceDelete(); - } else { - throw new \InvalidArgumentException( - "해당 직원의 {$data['pay_year']}년 {$data['pay_month']}월 급여가 이미 등록되어 있습니다." - ); - } - } - return DB::transaction(function () use ($data, $tenantId) { + // 동일 대상/기간 중복 체크 (트랜잭션 내 + 행 잠금으로 Race Condition 방지) + $existing = Payroll::withTrashed() + ->where('tenant_id', $tenantId) + ->where('user_id', $data['user_id']) + ->where('pay_year', $data['pay_year']) + ->where('pay_month', $data['pay_month']) + ->lockForUpdate() + ->first(); + + if ($existing) { + if ($existing->trashed()) { + $existing->forceDelete(); + } else { + throw new \InvalidArgumentException( + "해당 직원의 {$data['pay_year']}년 {$data['pay_month']}월 급여가 이미 등록되어 있습니다." + ); + } + } $familyCount = $data['family_count'] ?? $this->resolveFamilyCount($data['user_id']); $calculated = $this->calculateAmounts($data, null, $familyCount); $this->applyDeductionOverrides($calculated, $data['deduction_overrides'] ?? null);