From a63b501964acee04233da9f9f5c3a2a1ca51d25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 5 Mar 2026 15:57:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[approval]=20=EA=B8=B0=EC=95=88?= =?UTF-8?q?=ED=95=A8=20=ED=9C=B4=EA=B0=80=EC=8B=A0=EC=B2=AD=20=E2=86=92=20?= =?UTF-8?q?=ED=9C=B4=EA=B0=80=EA=B4=80=EB=A6=AC=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기안함에서 휴가/근태신청/사유서 양식 선택 시 전용 입력 폼 표시 - 양식코드별 유형 필터링 (leave/attendance_request/reason_report) - saveApproval()에서 content에 구조화된 데이터 포함 - handleApprovalCompleted()에서 Leave 없을 시 자동 생성 - createLeaveFromApproval() 메서드 추가 --- app/Http/Controllers/ApprovalController.php | 4 +- app/Services/ApprovalService.php | 39 +++++ resources/views/approvals/create.blade.php | 136 +++++++++++++++++- .../approvals/partials/_leave-form.blade.php | 57 ++++++++ 4 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 resources/views/approvals/partials/_leave-form.blade.php diff --git a/app/Http/Controllers/ApprovalController.php b/app/Http/Controllers/ApprovalController.php index d2d8d01f..c9ed3a89 100644 --- a/app/Http/Controllers/ApprovalController.php +++ b/app/Http/Controllers/ApprovalController.php @@ -5,6 +5,7 @@ use App\Models\Finance\BankAccount; use App\Models\Finance\CorporateCard; use App\Services\ApprovalService; +use App\Services\HR\LeaveService; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\View\View; @@ -39,8 +40,9 @@ public function create(Request $request): View|Response $forms = $this->service->getApprovalForms(); $lines = $this->service->getApprovalLines(); [$cards, $accounts] = $this->getCardAndAccountData(); + $employees = app(LeaveService::class)->getActiveEmployees(); - return view('approvals.create', compact('forms', 'lines', 'cards', 'accounts')); + return view('approvals.create', compact('forms', 'lines', 'cards', 'accounts', 'employees')); } /** diff --git a/app/Services/ApprovalService.php b/app/Services/ApprovalService.php index 6f795135..7fe77405 100644 --- a/app/Services/ApprovalService.php +++ b/app/Services/ApprovalService.php @@ -801,6 +801,39 @@ public function markCompletedAsRead(int $userId): int // Private 헬퍼 // ========================================================================= + /** + * 기안함에서 직접 올린 결재 → Leave 레코드 자동 생성 + */ + private function createLeaveFromApproval(Approval $approval): \App\Models\HR\Leave + { + $content = $approval->content; + $leaveType = $content['leave_type']; + $leaveService = app(\App\Services\HR\LeaveService::class); + + // 사유서는 days=0, 그 외는 자동 계산 + if (in_array($leaveType, \App\Models\HR\Leave::REASON_REPORT_TYPES)) { + $days = 0; + } else { + $days = $leaveService->calculateDays( + $leaveType, $content['start_date'], $content['end_date'] + ); + } + + return \App\Models\HR\Leave::create([ + 'tenant_id' => $approval->tenant_id, + 'user_id' => $content['user_id'] ?? $approval->drafter_id, + 'leave_type' => $leaveType, + 'start_date' => $content['start_date'], + 'end_date' => $content['end_date'], + 'days' => $days, + 'reason' => $content['reason'] ?? null, + 'status' => 'pending', + 'approval_id' => $approval->id, + 'created_by' => $approval->drafter_id, + 'updated_by' => $approval->drafter_id, + ]); + } + /** * 휴가/근태신청/사유서 관련 결재 양식인지 확인 */ @@ -819,6 +852,12 @@ private function handleApprovalCompleted(Approval $approval): void } $leave = \App\Models\HR\Leave::where('approval_id', $approval->id)->first(); + + // 기안함에서 직접 올린 경우: Leave 레코드 자동 생성 + if (! $leave && ! empty($approval->content['leave_type'])) { + $leave = $this->createLeaveFromApproval($approval); + } + if ($leave && $leave->status === 'pending') { app(\App\Services\HR\LeaveService::class)->approveByApproval($leave, $approval); } diff --git a/resources/views/approvals/create.blade.php b/resources/views/approvals/create.blade.php index 55d5e0bc..7f4533da 100644 --- a/resources/views/approvals/create.blade.php +++ b/resources/views/approvals/create.blade.php @@ -90,6 +90,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline- + {{-- 휴가/근태신청/사유서 전용 폼 --}} + @include('approvals.partials._leave-form', [ + 'employees' => $employees ?? collect(), + ]) + {{-- 지출결의서 전용 폼 --}} @include('approvals.partials._expense-form', [ 'initialData' => [], @@ -211,6 +216,84 @@ class="p-1 text-gray-400 hover:text-gray-600 transition"> const formCodes = @json($forms->pluck('code', 'id')); const linesData = @json($lines); let isExpenseForm = false; +let isLeaveForm = false; + +// 양식코드별 표시할 유형 목록 +const leaveTypesByFormCode = { + leave: [ + { value: 'annual', label: '연차' }, + { value: 'half_am', label: '오전반차' }, + { value: 'half_pm', label: '오후반차' }, + { value: 'sick', label: '병가' }, + { value: 'family', label: '경조사' }, + { value: 'maternity', label: '출산' }, + { value: 'parental', label: '육아' }, + ], + attendance_request: [ + { value: 'business_trip', label: '출장' }, + { value: 'remote', label: '재택근무' }, + { value: 'field_work', label: '외근' }, + { value: 'early_leave', label: '조퇴' }, + ], + reason_report: [ + { value: 'late_reason', label: '지각사유서' }, + { value: 'absent_reason', label: '결근사유서' }, + ], +}; + +function populateLeaveTypes(formCode) { + const select = document.getElementById('leave-type'); + select.innerHTML = ''; + const types = leaveTypesByFormCode[formCode] || []; + types.forEach(t => { + const opt = document.createElement('option'); + opt.value = t.value; + opt.textContent = t.label; + select.appendChild(opt); + }); +} + +function getLeaveFormData() { + return { + user_id: parseInt(document.getElementById('leave-user-id').value), + leave_type: document.getElementById('leave-type').value, + start_date: document.getElementById('leave-start-date').value, + end_date: document.getElementById('leave-end-date').value, + reason: document.getElementById('leave-reason').value.trim() || null, + }; +} + +function buildLeaveBody(data) { + const typeSelect = document.getElementById('leave-type'); + const userSelect = document.getElementById('leave-user-id'); + const typeName = typeSelect.options[typeSelect.selectedIndex]?.text || data.leave_type; + const userName = userSelect.options[userSelect.selectedIndex]?.text || ''; + + const rows = [ + ['신청자', userName], + ['유형', typeName], + ]; + + if (data.start_date === data.end_date) { + rows.push(['대상일', data.start_date]); + } else { + rows.push(['기간', data.start_date + ' ~ ' + data.end_date]); + } + + if (data.reason) { + rows.push(['사유', data.reason]); + } + + let html = '

아래와 같이 신청합니다.

'; + html += ''; + rows.forEach(([label, value]) => { + html += ''; + }); + html += '
' + + escapeHtml(label) + '' + + escapeHtml(value) + '
'; + return html; +} function escapeHtml(str) { if (!str) return ''; @@ -375,17 +458,39 @@ function applyQuickLine(lineId) { function switchFormMode(formId) { const code = formCodes[formId]; const expenseContainer = document.getElementById('expense-form-container'); + const leaveContainer = document.getElementById('leave-form-container'); const bodyArea = document.getElementById('body-area'); const expenseLoadBtn = document.getElementById('expense-load-btn'); + const leaveFormCodes = ['leave', 'attendance_request', 'reason_report']; + if (code === 'expense') { isExpenseForm = true; + isLeaveForm = false; expenseContainer.style.display = ''; + leaveContainer.style.display = 'none'; expenseLoadBtn.style.display = ''; bodyArea.style.display = 'none'; + } else if (leaveFormCodes.includes(code)) { + isExpenseForm = false; + isLeaveForm = true; + expenseContainer.style.display = 'none'; + leaveContainer.style.display = ''; + expenseLoadBtn.style.display = 'none'; + bodyArea.style.display = 'none'; + populateLeaveTypes(code); + + // 시작일/종료일 기본값: 오늘 + const today = new Date().toISOString().slice(0, 10); + const startEl = document.getElementById('leave-start-date'); + const endEl = document.getElementById('leave-end-date'); + if (!startEl.value) startEl.value = today; + if (!endEl.value) endEl.value = today; } else { isExpenseForm = false; + isLeaveForm = false; expenseContainer.style.display = 'none'; + leaveContainer.style.display = 'none'; expenseLoadBtn.style.display = 'none'; bodyArea.style.display = ''; } @@ -396,7 +501,7 @@ function applyBodyTemplate(formId) { switchFormMode(formId); // 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀 - if (isExpenseForm) { + if (isExpenseForm || isLeaveForm) { const titleEl = document.getElementById('title'); if (!titleEl.value.trim()) { const formSelect = document.getElementById('form_id'); @@ -474,21 +579,42 @@ function applyBodyTemplate(formId) { return; } - let expenseContent = {}; + let formContent = {}; + let formBody = getBodyContent(); let attachmentFileIds = []; + if (isExpenseForm) { const expenseEl = document.getElementById('expense-form-container'); if (expenseEl && expenseEl._x_dataStack) { - expenseContent = expenseEl._x_dataStack[0].getFormData(); + formContent = expenseEl._x_dataStack[0].getFormData(); attachmentFileIds = expenseEl._x_dataStack[0].getFileIds(); } + formBody = null; + } else if (isLeaveForm) { + const leaveData = getLeaveFormData(); + + if (!leaveData.leave_type) { + showToast('유형을 선택해주세요.', 'warning'); + return; + } + if (!leaveData.start_date || !leaveData.end_date) { + showToast('시작일과 종료일을 입력해주세요.', 'warning'); + return; + } + if (leaveData.start_date > leaveData.end_date) { + showToast('종료일이 시작일보다 이전입니다.', 'warning'); + return; + } + + formContent = leaveData; + formBody = buildLeaveBody(leaveData); } const payload = { form_id: document.getElementById('form_id').value, title: title, - body: isExpenseForm ? null : getBodyContent(), - content: isExpenseForm ? expenseContent : {}, + body: formBody, + content: formContent, attachment_file_ids: attachmentFileIds, is_urgent: document.getElementById('is_urgent').checked, steps: steps, diff --git a/resources/views/approvals/partials/_leave-form.blade.php b/resources/views/approvals/partials/_leave-form.blade.php new file mode 100644 index 00000000..cb4a5a8b --- /dev/null +++ b/resources/views/approvals/partials/_leave-form.blade.php @@ -0,0 +1,57 @@ +{{-- + 휴가/근태신청/사유서 전용 폼 + Props: + $employees (Collection) - 활성 사원 목록 +--}} +@php + $employees = $employees ?? collect(); + $currentUserId = auth()->id(); +@endphp + +