From 63271ed18c1a185ecc60e1d0ce2fc76db7529ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 10 Mar 2026 11:30:13 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20[payroll]=20=EC=A0=84=ED=91=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EA=B8=B0=ED=83=80=EA=B3=B5?= =?UTF-8?q?=EC=A0=9C=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기타공제(deductions JSON) 항목을 대변 207 예수금에 반영 - 차대 불균형 시 상세 분개 내역을 에러 모달로 표시 - toast 대신 복사 가능한 모달로 에러 메시지 표시 --- .../Api/Admin/HR/PayrollController.php | 72 +++++++++++++++---- resources/views/hr/payrolls/index.blade.php | 55 +++++++++++++- 2 files changed, 110 insertions(+), 17 deletions(-) diff --git a/app/Http/Controllers/Api/Admin/HR/PayrollController.php b/app/Http/Controllers/Api/Admin/HR/PayrollController.php index 0a5d6e93..a2213795 100644 --- a/app/Http/Controllers/Api/Admin/HR/PayrollController.php +++ b/app/Http/Controllers/Api/Admin/HR/PayrollController.php @@ -738,27 +738,44 @@ public function generateJournalEntry(Request $request): JsonResponse } // 해당월 급여 합산 - $sums = Payroll::forTenant($tenantId) + $payrolls = Payroll::forTenant($tenantId) ->forPeriod($year, $month) - ->selectRaw(' - SUM(gross_salary) as total_gross, - SUM(pension) as total_pension, - SUM(health_insurance) as total_health, - SUM(long_term_care) as total_ltc, - SUM(employment_insurance) as total_emp, - SUM(income_tax) as total_income_tax, - SUM(resident_tax) as total_resident_tax, - SUM(net_salary) as total_net - ') - ->first(); + ->get(); - if (! $sums || (int) $sums->total_gross === 0) { + if ($payrolls->isEmpty()) { return response()->json([ 'success' => false, 'message' => '해당 월 급여 데이터가 없습니다.', ], 422); } + // 기타공제(deductions JSON) 합산 포함 + $extraDeductionsTotal = 0; + foreach ($payrolls as $p) { + foreach ($p->deductions ?? [] as $d) { + $extraDeductionsTotal += (int) ($d['amount'] ?? 0); + } + } + + $sums = (object) [ + 'total_gross' => $payrolls->sum('gross_salary'), + 'total_pension' => $payrolls->sum('pension'), + 'total_health' => $payrolls->sum('health_insurance'), + 'total_ltc' => $payrolls->sum('long_term_care'), + 'total_emp' => $payrolls->sum('employment_insurance'), + 'total_income_tax' => $payrolls->sum('income_tax'), + 'total_resident_tax' => $payrolls->sum('resident_tax'), + 'total_net' => $payrolls->sum('net_salary'), + 'total_extra_deductions' => $extraDeductionsTotal, + ]; + + if ((int) $sums->total_gross === 0) { + return response()->json([ + 'success' => false, + 'message' => '해당 월 급여 데이터의 총지급액이 0입니다.', + ], 422); + } + // 거래처 조회 $partnerNames = ['임직원', '건강보험연금', '건강보험건강', '건강보험고용', '강서세무서', '강서구청']; $partners = TradingPartner::forTenant($tenantId) @@ -906,7 +923,23 @@ public function generateJournalEntry(Request $request): JsonResponse ]; } - // 8. 대변: 205 미지급비용 / 임직원 — 급여 + // 8. 대변: 207 예수금 / 임직원 — 기타공제 + $extraDeductions = (int) $sums->total_extra_deductions; + if ($extraDeductions > 0) { + $lines[] = [ + 'dc_type' => 'credit', + 'account_code' => '207', + 'account_name' => $accountCodes['207'], + 'trading_partner_id' => $partners['임직원'], + 'trading_partner_name' => '임직원', + 'debit_amount' => 0, + 'credit_amount' => $extraDeductions, + 'description' => '기타공제', + 'line_no' => $lineNo++, + ]; + } + + // 9. 대변: 205 미지급비용 / 임직원 — 급여 $netSalary = (int) $sums->total_net; if ($netSalary > 0) { $lines[] = [ @@ -927,9 +960,18 @@ public function generateJournalEntry(Request $request): JsonResponse $totalCredit = collect($lines)->sum('credit_amount'); if ($totalDebit !== $totalCredit || $totalDebit === 0) { + $detail = collect($lines)->map(fn ($l) => sprintf( + '[%s] %s %s: %s', + $l['dc_type'], + $l['account_code'], + $l['description'], + number_format($l['dc_type'] === 'debit' ? $l['debit_amount'] : $l['credit_amount']) + ))->implode("\n"); + return response()->json([ 'success' => false, - 'message' => "차변({$totalDebit})과 대변({$totalCredit})이 일치하지 않습니다.", + 'message' => '차변('.number_format($totalDebit).')과 대변('.number_format($totalCredit).")이 일치하지 않습니다.\n차이: ".number_format(abs($totalDebit - $totalCredit)), + 'detail' => "급여 {$payrolls->count()}건 합산\n\n{$detail}", ], 422); } diff --git a/resources/views/hr/payrolls/index.blade.php b/resources/views/hr/payrolls/index.blade.php index 644f476a..7e3a9cae 100644 --- a/resources/views/hr/payrolls/index.blade.php +++ b/resources/views/hr/payrolls/index.blade.php @@ -841,6 +841,31 @@ class="px-5 py-2 text-sm text-white bg-violet-600 hover:bg-violet-700 rounded-lg +{{-- 에러 상세 모달 --}} + @endsection @push('scripts') @@ -941,12 +966,12 @@ function generateJournalEntry() { if (result.success) { showToast(result.message, 'success'); } else { - showToast(result.message, 'error'); + showErrorModal(result.message, result.detail || null); } }) .catch(err => { console.error(err); - showToast('전표 생성 중 오류가 발생했습니다.', 'error'); + showErrorModal('전표 생성 중 오류가 발생했습니다.', err.message); }); } @@ -1780,6 +1805,32 @@ function printPayslipPreview() { win.print(); } + // ===== 에러 모달 ===== + function showErrorModal(message, detail) { + document.getElementById('errorDetailMessage').textContent = message; + const contentEl = document.getElementById('errorDetailContent'); + if (detail) { + contentEl.textContent = detail; + contentEl.classList.remove('hidden'); + } else { + contentEl.classList.add('hidden'); + } + document.getElementById('errorDetailModal').classList.remove('hidden'); + } + + function closeErrorModal() { + document.getElementById('errorDetailModal').classList.add('hidden'); + } + + function copyErrorDetail() { + const message = document.getElementById('errorDetailMessage').textContent; + const detail = document.getElementById('errorDetailContent').textContent; + const text = detail ? message + '\n\n' + detail : message; + navigator.clipboard.writeText(text).then(() => { + showToast('클립보드에 복사되었습니다.', 'success'); + }); + } + // ===== 토스트 메시지 ===== function showToast(message, type) { if (typeof window.showToastMessage === 'function') {