diff --git a/app/Http/Controllers/Api/Admin/ApprovalApiController.php b/app/Http/Controllers/Api/Admin/ApprovalApiController.php index 5dcccd36..058d2a67 100644 --- a/app/Http/Controllers/Api/Admin/ApprovalApiController.php +++ b/app/Http/Controllers/Api/Admin/ApprovalApiController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Models\Boards\File; use App\Services\ApprovalService; +use App\Services\EmploymentCertService; use App\Services\GoogleCloudStorageService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -254,6 +255,74 @@ public function bulkDestroy(Request $request): JsonResponse ]); } + // ========================================================================= + // 재직증명서 + // ========================================================================= + + /** + * 사원 재직증명서 정보 조회 + */ + public function certInfo(int $userId): JsonResponse + { + try { + $tenantId = session('selected_tenant_id'); + $service = app(EmploymentCertService::class); + $data = $service->getCertInfo($userId, $tenantId); + + return response()->json(['success' => true, 'data' => $data]); + } catch (\Throwable $e) { + return response()->json([ + 'success' => false, + 'message' => '사원 정보를 불러올 수 없습니다.', + ], 400); + } + } + + /** + * 재직증명서 DOCX 생성 + */ + public function generateCertDocx(Request $request): JsonResponse + { + $request->validate([ + 'user_id' => 'required|integer', + 'purpose' => 'required|string|max:200', + 'address' => 'nullable|string|max:500', + ]); + + try { + $tenantId = session('selected_tenant_id'); + $service = app(EmploymentCertService::class); + $certInfo = $service->getCertInfo($request->input('user_id'), $tenantId); + + // 주소가 수정된 경우 덮어쓰기 + if ($request->filled('address')) { + $certInfo['address'] = $request->input('address'); + } + $certInfo['purpose'] = $request->input('purpose'); + + $storagePath = $service->generateDocx($certInfo, $tenantId); + $displayName = '재직증명서_'.($certInfo['name'] ?? '').'_'.date('Ymd').'.docx'; + $fileRecord = $service->createFileRecord($storagePath, $displayName, $tenantId); + + return response()->json([ + 'success' => true, + 'data' => [ + 'file_id' => $fileRecord->id, + 'file_name' => $displayName, + ], + 'message' => '재직증명서가 생성되었습니다.', + ]); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => '재직증명서 생성에 실패했습니다.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + // ========================================================================= // 워크플로우 // ========================================================================= diff --git a/app/Http/Controllers/ApprovalController.php b/app/Http/Controllers/ApprovalController.php index c9ed3a89..c15d5d5e 100644 --- a/app/Http/Controllers/ApprovalController.php +++ b/app/Http/Controllers/ApprovalController.php @@ -63,8 +63,9 @@ public function edit(Request $request, int $id): View|Response $forms = $this->service->getApprovalForms(); $lines = $this->service->getApprovalLines(); [$cards, $accounts] = $this->getCardAndAccountData(); + $employees = app(LeaveService::class)->getActiveEmployees(); - return view('approvals.edit', compact('approval', 'forms', 'lines', 'cards', 'accounts')); + return view('approvals.edit', compact('approval', 'forms', 'lines', 'cards', 'accounts', 'employees')); } /** diff --git a/app/Services/EmploymentCertService.php b/app/Services/EmploymentCertService.php new file mode 100644 index 00000000..c5f9cb1e --- /dev/null +++ b/app/Services/EmploymentCertService.php @@ -0,0 +1,121 @@ +where('tenant_id', $tenantId) + ->where('user_id', $userId) + ->with(['user', 'department']) + ->firstOrFail(); + + $tenant = Tenant::findOrFail($tenantId); + + $residentNumber = $employee->resident_number; + $maskedResident = $residentNumber + ? substr($residentNumber, 0, 8).'******' + : ''; + + return [ + 'name' => $employee->user->name ?? $employee->display_name ?? '', + 'resident_number' => $maskedResident, + 'resident_number_full' => $residentNumber ?? '', + 'address' => $employee->address ?? '', + 'department' => $employee->department?->name ?? '', + 'position' => $employee->position_label ?? '', + 'hire_date' => $employee->hire_date ?? '', + 'company_name' => $tenant->company_name ?? '', + 'business_num' => $tenant->business_num ?? '', + 'ceo_name' => $tenant->ceo_name ?? '', + 'phone' => $tenant->phone ?? '', + 'company_address' => $tenant->address ?? '', + ]; + } + + /** + * DOCX 생성 + */ + public function generateDocx(array $data, int $tenantId): string + { + $templatePath = storage_path('app/templates/employment_cert.docx'); + + if (! file_exists($templatePath)) { + throw new \RuntimeException('재직증명서 템플릿 파일이 없습니다.'); + } + + $template = new TemplateProcessor($templatePath); + + $hireDateFormatted = ''; + if (! empty($data['hire_date'])) { + try { + $hireDateFormatted = date('Y년 m월 d일', strtotime($data['hire_date'])); + } catch (\Throwable) { + $hireDateFormatted = $data['hire_date']; + } + } + + $issueDateFormatted = date('Y년 m월 d일'); + + $template->setValue('name', $data['name'] ?? ''); + $template->setValue('resident_number', $data['resident_number_full'] ?? ''); + $template->setValue('address', $data['address'] ?? ''); + $template->setValue('company_name', $data['company_name'] ?? ''); + $template->setValue('business_num', $data['business_num'] ?? ''); + $template->setValue('ceo_name', $data['ceo_name'] ?? ''); + $template->setValue('phone', $data['phone'] ?? ''); + $template->setValue('company_address', $data['company_address'] ?? ''); + $template->setValue('department', $data['department'] ?? ''); + $template->setValue('position', $data['position'] ?? ''); + $template->setValue('hire_date', $hireDateFormatted); + $template->setValue('purpose', $data['purpose'] ?? ''); + $template->setValue('issue_date', $issueDateFormatted); + + $outputDir = storage_path("app/approvals/{$tenantId}"); + if (! is_dir($outputDir)) { + mkdir($outputDir, 0755, true); + } + + $fileName = '재직증명서_'.($data['name'] ?? 'unknown').'_'.date('Ymd').'.docx'; + $storedName = Str::random(40).'.docx'; + $storagePath = "approvals/{$tenantId}/{$storedName}"; + $fullPath = storage_path("app/{$storagePath}"); + + $template->saveAs($fullPath); + + return $storagePath; + } + + /** + * 파일 레코드 생성 및 approval에 첨부 + */ + public function createFileRecord(string $storagePath, string $displayName, int $tenantId): File + { + $fullPath = storage_path("app/{$storagePath}"); + + return File::create([ + 'tenant_id' => $tenantId, + 'document_type' => 'approval_attachment', + 'display_name' => $displayName, + 'original_name' => $displayName, + 'stored_name' => basename($storagePath), + 'file_path' => $storagePath, + 'mime_type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'file_size' => filesize($fullPath), + 'file_type' => 'docx', + 'is_temp' => false, + 'uploaded_by' => auth()->id(), + ]); + } +} diff --git a/composer.json b/composer.json index f029a93a..968c5dc7 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "laravel/sanctum": "^4.2", "laravel/tinker": "^2.10.1", "phpoffice/phpspreadsheet": "^5.4", + "phpoffice/phpword": "^1.4", "setasign/fpdi": "^2.6", "spatie/laravel-permission": "^6.23", "stevebauman/purify": "^6.3", diff --git a/composer.lock b/composer.lock index eb82bf64..19840fd5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fa477884dd95c46333bc82ce0a64e4fb", + "content-hash": "36c1111268937924cbb3cad512a30a41", "packages": [ { "name": "brick/math", @@ -3197,6 +3197,58 @@ ], "time": "2025-11-20T02:34:59+00:00" }, + { + "name": "phpoffice/math", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "homepage": "https://phpoffice.github.io/Math/", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/0.3.0" + }, + "time": "2025-05-29T08:31:49+00:00" + }, { "name": "phpoffice/phpspreadsheet", "version": "5.4.0", @@ -3306,6 +3358,114 @@ }, "time": "2026-01-11T04:52:00+00:00" }, + { + "name": "phpoffice/phpword", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPWord.git", + "reference": "6d75328229bc93790b37e93741adf70646cea958" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", + "reference": "6d75328229bc93790b37e93741adf70646cea958", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-gd": "*", + "ext-json": "*", + "ext-xml": "*", + "ext-zip": "*", + "php": "^7.1|^8.0", + "phpoffice/math": "^0.3" + }, + "require-dev": { + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-libxml": "*", + "friendsofphp/php-cs-fixer": "^3.3", + "mpdf/mpdf": "^7.0 || ^8.0", + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": ">=7.0", + "symfony/process": "^4.4 || ^5.0", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Allows writing PDF", + "ext-xmlwriter": "Allows writing OOXML and ODF", + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpWord\\": "src/PhpWord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Mark Baker" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com", + "homepage": "http://gabrielbull.com/" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net/blog/" + }, + { + "name": "Ivan Lanin", + "homepage": "http://ivan.lanin.org" + }, + { + "name": "Roman Syroeshko", + "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" + }, + { + "name": "Antoine de Troostembergh" + } + ], + "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", + "homepage": "https://phpoffice.github.io/PHPWord/", + "keywords": [ + "ISO IEC 29500", + "OOXML", + "Office Open XML", + "OpenDocument", + "OpenXML", + "PhpOffice", + "PhpWord", + "Rich Text Format", + "WordprocessingML", + "doc", + "docx", + "html", + "odf", + "odt", + "office", + "pdf", + "php", + "reader", + "rtf", + "template", + "template processor", + "word", + "writer" + ], + "support": { + "issues": "https://github.com/PHPOffice/PHPWord/issues", + "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" + }, + "time": "2025-06-05T10:32:36+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.4", diff --git a/resources/views/approvals/create.blade.php b/resources/views/approvals/create.blade.php index 7f4533da..1260082f 100644 --- a/resources/views/approvals/create.blade.php +++ b/resources/views/approvals/create.blade.php @@ -95,6 +95,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline- 'employees' => $employees ?? collect(), ]) + {{-- 재직증명서 전용 폼 --}} + @include('approvals.partials._certificate-form', [ + 'employees' => $employees ?? collect(), + ]) + {{-- 지출결의서 전용 폼 --}} @include('approvals.partials._expense-form', [ 'initialData' => [], @@ -217,6 +222,8 @@ class="p-1 text-gray-400 hover:text-gray-600 transition"> const linesData = @json($lines); let isExpenseForm = false; let isLeaveForm = false; +let isCertForm = false; +let certFileId = null; // 양식코드별 표시할 유형 목록 const leaveTypesByFormCode = { @@ -459,39 +466,44 @@ function switchFormMode(formId) { const code = formCodes[formId]; const expenseContainer = document.getElementById('expense-form-container'); const leaveContainer = document.getElementById('leave-form-container'); + const certContainer = document.getElementById('cert-form-container'); const bodyArea = document.getElementById('body-area'); const expenseLoadBtn = document.getElementById('expense-load-btn'); const leaveFormCodes = ['leave', 'attendance_request', 'reason_report']; + // 모두 숨기기 + expenseContainer.style.display = 'none'; + leaveContainer.style.display = 'none'; + certContainer.style.display = 'none'; + expenseLoadBtn.style.display = 'none'; + bodyArea.style.display = 'none'; + isExpenseForm = false; + isLeaveForm = false; + isCertForm = false; + 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 if (code === 'employment_cert') { + isCertForm = true; + certContainer.style.display = ''; + certFileId = null; + // 초기 사원 정보 로드 + const certUserId = document.getElementById('cert-user-id').value; + if (certUserId) loadCertInfo(certUserId); } else { - isExpenseForm = false; - isLeaveForm = false; - expenseContainer.style.display = 'none'; - leaveContainer.style.display = 'none'; - expenseLoadBtn.style.display = 'none'; bodyArea.style.display = ''; } } @@ -501,7 +513,7 @@ function applyBodyTemplate(formId) { switchFormMode(formId); // 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀 - if (isExpenseForm || isLeaveForm) { + if (isExpenseForm || isLeaveForm || isCertForm) { const titleEl = document.getElementById('title'); if (!titleEl.value.trim()) { const formSelect = document.getElementById('form_id'); @@ -608,6 +620,56 @@ function applyBodyTemplate(formId) { formContent = leaveData; formBody = buildLeaveBody(leaveData); + } else if (isCertForm) { + const purpose = getCertPurpose(); + if (!purpose) { + showToast('사용용도를 입력해주세요.', 'warning'); + return; + } + + formContent = { + cert_user_id: document.getElementById('cert-user-id').value, + name: document.getElementById('cert-name').value, + resident_number: document.getElementById('cert-resident').value, + address: document.getElementById('cert-address').value, + department: document.getElementById('cert-department').value, + position: document.getElementById('cert-position').value, + hire_date: document.getElementById('cert-hire-date').value, + company_name: document.getElementById('cert-company').value, + business_num: document.getElementById('cert-business-num').value, + purpose: purpose, + issue_date: document.getElementById('cert-issue-date').value, + }; + formBody = null; + + // DOCX 생성 + if (!certFileId) { + showToast('재직증명서 DOCX를 생성 중입니다...', 'info'); + try { + const certResp = await fetch('/api/admin/approvals/generate-cert-docx', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ + user_id: formContent.cert_user_id, + purpose: purpose, + address: formContent.address, + }), + }); + const certData = await certResp.json(); + if (certData.success) { + certFileId = certData.data.file_id; + } else { + showToast(certData.message || 'DOCX 생성 실패', 'error'); + return; + } + } catch (e) { + showToast('DOCX 생성 중 오류가 발생했습니다.', 'error'); + return; + } + } + if (certFileId) { + attachmentFileIds.push(certFileId); + } } const payload = { @@ -787,6 +849,58 @@ function closeExpenseLoadModal() { } } +// ========================================================================= +// 재직증명서 관련 함수 +// ========================================================================= + +async function loadCertInfo(userId) { + if (!userId) return; + + try { + const resp = await fetch(`/api/admin/approvals/cert-info/${userId}`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }); + const data = await resp.json(); + + if (data.success) { + const d = data.data; + document.getElementById('cert-name').value = d.name || ''; + document.getElementById('cert-resident').value = d.resident_number || ''; + document.getElementById('cert-address').value = d.address || ''; + document.getElementById('cert-company').value = d.company_name || ''; + document.getElementById('cert-business-num').value = d.business_num || ''; + document.getElementById('cert-department').value = d.department || ''; + document.getElementById('cert-position').value = d.position || ''; + document.getElementById('cert-hire-date').value = d.hire_date ? d.hire_date + ' ~' : ''; + certFileId = null; // 사원 변경 시 DOCX 재생성 필요 + } else { + showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error'); + } + } catch (e) { + showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error'); + } +} + +function onCertPurposeChange() { + const sel = document.getElementById('cert-purpose-select'); + const customWrap = document.getElementById('cert-purpose-custom-wrap'); + if (sel.value === '__custom__') { + customWrap.style.display = ''; + document.getElementById('cert-purpose-custom').focus(); + } else { + customWrap.style.display = 'none'; + } + certFileId = null; // 용도 변경 시 DOCX 재생성 필요 +} + +function getCertPurpose() { + const sel = document.getElementById('cert-purpose-select'); + if (sel.value === '__custom__') { + return document.getElementById('cert-purpose-custom').value.trim(); + } + return sel.value; +} + @endpush diff --git a/resources/views/approvals/edit.blade.php b/resources/views/approvals/edit.blade.php index 2ea279cf..643d9b04 100644 --- a/resources/views/approvals/edit.blade.php +++ b/resources/views/approvals/edit.blade.php @@ -126,6 +126,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline- + {{-- 재직증명서 전용 폼 --}} + @include('approvals.partials._certificate-form', [ + 'employees' => $employees ?? collect(), + ]) + {{-- 지출결의서 전용 폼 --}} @php $existingFiles = []; @@ -254,6 +259,8 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon const formCodes = @json($forms->pluck('code', 'id')); const linesData = @json($lines); let isExpenseForm = false; +let isCertForm = false; +let certFileId = null; function escapeHtml(str) { if (!str) return ''; @@ -418,15 +425,25 @@ function applyQuickLine(lineId) { function switchFormMode(formId) { const code = formCodes[formId]; const expenseContainer = document.getElementById('expense-form-container'); + const certContainer = document.getElementById('cert-form-container'); const bodyArea = document.getElementById('body-area'); + expenseContainer.style.display = 'none'; + certContainer.style.display = 'none'; + bodyArea.style.display = 'none'; + isExpenseForm = false; + isCertForm = false; + if (code === 'expense') { isExpenseForm = true; expenseContainer.style.display = ''; - bodyArea.style.display = 'none'; + } else if (code === 'employment_cert') { + isCertForm = true; + certContainer.style.display = ''; + certFileId = null; + const certUserId = document.getElementById('cert-user-id').value; + if (certUserId) loadCertInfo(certUserId); } else { - isExpenseForm = false; - expenseContainer.style.display = 'none'; bodyArea.style.display = ''; } } @@ -436,7 +453,7 @@ function applyBodyTemplate(formId) { switchFormMode(formId); // 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀 - if (isExpenseForm) { + if (isExpenseForm || isCertForm) { const titleEl = document.getElementById('title'); if (!titleEl.value.trim()) { const formSelect = document.getElementById('form_id'); @@ -487,8 +504,41 @@ function applyBodyTemplate(formId) { // 초기 양식에 대한 폼 모드 전환 switchFormMode(document.getElementById('form_id').value); + // 재직증명서 기존 데이터 복원 + if (isCertForm) { + const certContent = @json($approval->content ?? []); + if (certContent.name) { + document.getElementById('cert-name').value = certContent.name || ''; + document.getElementById('cert-resident').value = certContent.resident_number || ''; + document.getElementById('cert-address').value = certContent.address || ''; + document.getElementById('cert-company').value = certContent.company_name || ''; + document.getElementById('cert-business-num').value = certContent.business_num || ''; + document.getElementById('cert-department').value = certContent.department || ''; + document.getElementById('cert-position').value = certContent.position || ''; + document.getElementById('cert-hire-date').value = certContent.hire_date || ''; + document.getElementById('cert-issue-date').value = certContent.issue_date || '{{ now()->format("Y-m-d") }}'; + + // 용도 복원 + const purposeSelect = document.getElementById('cert-purpose-select'); + const purpose = certContent.purpose || ''; + const predefined = ['은행 제출용', '관공서 제출용', '비자 신청용', '대출 신청용']; + if (predefined.includes(purpose)) { + purposeSelect.value = purpose; + } else if (purpose) { + purposeSelect.value = '__custom__'; + document.getElementById('cert-purpose-custom-wrap').style.display = ''; + document.getElementById('cert-purpose-custom').value = purpose; + } + + // 사원 셀렉트 복원 + if (certContent.cert_user_id) { + document.getElementById('cert-user-id').value = certContent.cert_user_id; + } + } + } + // 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화 - if (!isExpenseForm) { + if (!isExpenseForm && !isCertForm) { const existingBody = document.getElementById('body').value; if (/<[a-z][\s\S]*>/i.test(existingBody)) { document.getElementById('useEditor').checked = true; @@ -520,21 +570,74 @@ 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 (isCertForm) { + const purpose = getCertPurpose(); + if (!purpose) { + showToast('사용용도를 입력해주세요.', 'warning'); + return; + } + + formContent = { + cert_user_id: document.getElementById('cert-user-id').value, + name: document.getElementById('cert-name').value, + resident_number: document.getElementById('cert-resident').value, + address: document.getElementById('cert-address').value, + department: document.getElementById('cert-department').value, + position: document.getElementById('cert-position').value, + hire_date: document.getElementById('cert-hire-date').value, + company_name: document.getElementById('cert-company').value, + business_num: document.getElementById('cert-business-num').value, + purpose: purpose, + issue_date: document.getElementById('cert-issue-date').value, + }; + formBody = null; + + // DOCX 생성 + if (!certFileId) { + showToast('재직증명서 DOCX를 생성 중입니다...', 'info'); + try { + const certResp = await fetch('/api/admin/approvals/generate-cert-docx', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ + user_id: formContent.cert_user_id, + purpose: purpose, + address: formContent.address, + }), + }); + const certData = await certResp.json(); + if (certData.success) { + certFileId = certData.data.file_id; + } else { + showToast(certData.message || 'DOCX 생성 실패', 'error'); + return; + } + } catch (e) { + showToast('DOCX 생성 중 오류가 발생했습니다.', 'error'); + return; + } + } + if (certFileId) { + attachmentFileIds.push(certFileId); + } } 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, @@ -608,5 +711,57 @@ function applyBodyTemplate(formId) { showToast('서버 오류가 발생했습니다.', 'error'); } } + +// ========================================================================= +// 재직증명서 관련 함수 +// ========================================================================= + +async function loadCertInfo(userId) { + if (!userId) return; + + try { + const resp = await fetch(`/api/admin/approvals/cert-info/${userId}`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' } + }); + const data = await resp.json(); + + if (data.success) { + const d = data.data; + document.getElementById('cert-name').value = d.name || ''; + document.getElementById('cert-resident').value = d.resident_number || ''; + document.getElementById('cert-address').value = d.address || ''; + document.getElementById('cert-company').value = d.company_name || ''; + document.getElementById('cert-business-num').value = d.business_num || ''; + document.getElementById('cert-department').value = d.department || ''; + document.getElementById('cert-position').value = d.position || ''; + document.getElementById('cert-hire-date').value = d.hire_date ? d.hire_date + ' ~' : ''; + certFileId = null; + } else { + showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error'); + } + } catch (e) { + showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error'); + } +} + +function onCertPurposeChange() { + const sel = document.getElementById('cert-purpose-select'); + const customWrap = document.getElementById('cert-purpose-custom-wrap'); + if (sel.value === '__custom__') { + customWrap.style.display = ''; + document.getElementById('cert-purpose-custom').focus(); + } else { + customWrap.style.display = 'none'; + } + certFileId = null; +} + +function getCertPurpose() { + const sel = document.getElementById('cert-purpose-select'); + if (sel.value === '__custom__') { + return document.getElementById('cert-purpose-custom').value.trim(); + } + return sel.value; +} @endpush diff --git a/resources/views/approvals/partials/_certificate-form.blade.php b/resources/views/approvals/partials/_certificate-form.blade.php new file mode 100644 index 00000000..4f71c182 --- /dev/null +++ b/resources/views/approvals/partials/_certificate-form.blade.php @@ -0,0 +1,124 @@ +{{-- + 재직증명서 전용 폼 + Props: + $employees (Collection) - 활성 사원 목록 +--}} +@php + $employees = $employees ?? collect(); + $currentUserId = auth()->id(); +@endphp + + diff --git a/resources/views/approvals/partials/_certificate-show.blade.php b/resources/views/approvals/partials/_certificate-show.blade.php new file mode 100644 index 00000000..dabc20da --- /dev/null +++ b/resources/views/approvals/partials/_certificate-show.blade.php @@ -0,0 +1,101 @@ +{{-- + 재직증명서 읽기전용 렌더링 + Props: + $content (array) - approvals.content JSON +--}} +
+ {{-- 인적사항 --}} +
+
+

1. 인적사항

+
+
+
+
+ 성명 +
{{ $content['name'] ?? '-' }}
+
+
+ 주민등록번호 +
{{ $content['resident_number'] ?? '-' }}
+
+
+ 주소 +
{{ $content['address'] ?? '-' }}
+
+
+
+
+ + {{-- 재직사항 --}} +
+
+

2. 재직사항

+
+
+
+
+ 회사명 +
{{ $content['company_name'] ?? '-' }}
+
+
+ 사업자번호 +
{{ $content['business_num'] ?? '-' }}
+
+
+ 근무부서 +
{{ $content['department'] ?? '-' }}
+
+
+ 직급 +
{{ $content['position'] ?? '-' }}
+
+
+ 재직기간 +
{{ $content['hire_date'] ?? '-' }}
+
+
+
+
+ + {{-- 발급정보 --}} +
+
+

3. 발급정보

+
+
+
+
+ 사용용도 +
{{ $content['purpose'] ?? '-' }}
+
+
+ 발급일 +
{{ $content['issue_date'] ?? '-' }}
+
+
+
+
+ + {{-- 첨부파일 --}} + @if(!empty($approval->attachments)) +
+ 첨부파일 +
+ @foreach($approval->attachments as $file) +
+ + + + + {{ $file['name'] ?? '파일' }} + + + {{ isset($file['size']) ? number_format($file['size'] / 1024, 1) . 'KB' : '' }} + +
+ @endforeach +
+
+ @endif +
diff --git a/resources/views/approvals/show.blade.php b/resources/views/approvals/show.blade.php index 71506fa6..8084a646 100644 --- a/resources/views/approvals/show.blade.php +++ b/resources/views/approvals/show.blade.php @@ -82,6 +82,8 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition

{{ $approval->title }}

@if(!empty($approval->content) && $approval->form?->code === 'expense') @include('approvals.partials._expense-show', ['content' => $approval->content]) + @elseif(!empty($approval->content) && $approval->form?->code === 'employment_cert') + @include('approvals.partials._certificate-show', ['content' => $approval->content]) @elseif($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
{!! strip_tags($approval->body, '