From ac8f16de599e6c2d0d29031d6fa87aa6bed63ff0 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 10:26:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[approvals]=20=EC=A7=80=EC=B6=9C?= =?UTF-8?q?=EA=B2=B0=EC=9D=98=EC=84=9C=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=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 - 기안 작성 시 '불러오기' 버튼으로 기존 지출결의서 불러오기 - 지출결의서 이력 API 엔드포인트 추가 (/expense-history) - 선택한 지출결의서의 내용을 새 폼에 복사 (날짜는 오늘로 초기화) --- .../Api/Admin/ApprovalApiController.php | 30 ++++ resources/views/approvals/create.blade.php | 164 ++++++++++++++++++ routes/api.php | 1 + 3 files changed, 195 insertions(+) diff --git a/app/Http/Controllers/Api/Admin/ApprovalApiController.php b/app/Http/Controllers/Api/Admin/ApprovalApiController.php index c4010be3..639c21b2 100644 --- a/app/Http/Controllers/Api/Admin/ApprovalApiController.php +++ b/app/Http/Controllers/Api/Admin/ApprovalApiController.php @@ -444,6 +444,36 @@ public function destroyLine(int $id): JsonResponse ]); } + /** + * 지출결의서 이력 (불러오기용) + */ + public function expenseHistory(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + $approvals = \App\Models\Approvals\Approval::where('tenant_id', $tenantId) + ->where('drafter_id', auth()->id()) + ->whereHas('form', fn ($q) => $q->where('code', 'expense')) + ->whereIn('status', ['draft', 'pending', 'approved', 'rejected', 'cancelled']) + ->whereNotNull('content') + ->orderByDesc('created_at') + ->limit(30) + ->get(['id', 'title', 'content', 'status', 'created_at']); + + $data = $approvals->map(fn ($a) => [ + 'id' => $a->id, + 'title' => $a->title, + 'status' => $a->status, + 'status_label' => $a->status_label, + 'total_amount' => $a->content['total_amount'] ?? 0, + 'expense_type' => $a->content['expense_type'] ?? '', + 'created_at' => $a->created_at->format('Y-m-d'), + 'content' => $a->content, + ]); + + return response()->json(['success' => true, 'data' => $data]); + } + /** * 양식 목록 */ diff --git a/resources/views/approvals/create.blade.php b/resources/views/approvals/create.blade.php index 428b58a8..e68b732f 100644 --- a/resources/views/approvals/create.blade.php +++ b/resources/views/approvals/create.blade.php @@ -82,6 +82,16 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline- {{-- 지출결의서 전용 폼 --}} + @include('approvals.partials._expense-form', [ 'initialData' => [], 'cards' => $cards ?? collect(), @@ -132,6 +142,28 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon + {{-- 지출결의서 불러오기 모달 --}} + @endsection @push('styles') @@ -345,14 +377,17 @@ function switchFormMode(formId) { const code = formCodes[formId]; const expenseContainer = document.getElementById('expense-form-container'); const bodyArea = document.getElementById('body-area'); + const expenseLoadArea = document.getElementById('expense-load-area'); if (code === 'expense') { isExpenseForm = true; expenseContainer.style.display = ''; + expenseLoadArea.style.display = ''; bodyArea.style.display = 'none'; } else { isExpenseForm = false; expenseContainer.style.display = 'none'; + expenseLoadArea.style.display = 'none'; bodyArea.style.display = ''; } } @@ -401,6 +436,11 @@ function applyBodyTemplate(formId) { document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { + const expModal = document.getElementById('expense-load-modal'); + if (expModal && expModal.style.display !== 'none') { + closeExpenseLoadModal(); + return; + } const modal = document.getElementById('approval-line-modal'); if (modal && modal.style.display !== 'none') { closeApprovalLineModal(); @@ -499,5 +539,129 @@ function applyBodyTemplate(formId) { showToast('서버 오류가 발생했습니다.', 'error'); } } + +// ========================================================================= +// 지출결의서 불러오기 +// ========================================================================= + +const expenseTypeLabels = { + corporate_card: '법인카드', transfer: '송금', auto_transfer: '자동이체 출금', + cash_advance: '현금/가지급정산', +}; +const statusColors = { + draft: 'bg-gray-100 text-gray-600', pending: 'bg-blue-100 text-blue-600', + approved: 'bg-green-100 text-green-600', rejected: 'bg-red-100 text-red-600', + cancelled: 'bg-yellow-100 text-yellow-600', +}; + +async function openExpenseLoadModal() { + const modal = document.getElementById('expense-load-modal'); + const list = document.getElementById('expense-load-list'); + modal.style.display = ''; + document.body.style.overflow = 'hidden'; + list.innerHTML = '
불러오는 중...
'; + + try { + const res = await fetch('/api/admin/approvals/expense-history', { + headers: { 'Accept': 'application/json' }, + }); + const json = await res.json(); + + if (!json.success || !json.data.length) { + list.innerHTML = '
이전 지출결의서가 없습니다.
'; + return; + } + + list.innerHTML = json.data.map(item => { + const amount = parseInt(item.total_amount || 0).toLocaleString('ko-KR'); + const typeLabel = expenseTypeLabels[item.expense_type] || item.expense_type; + const color = statusColors[item.status] || statusColors.draft; + return `
+
+
+ ${escapeHtml(item.title)} + ${escapeHtml(item.status_label)} +
+
+ ${escapeHtml(item.created_at)} + ${escapeHtml(typeLabel)} + ${amount}원 +
+
+ + + +
`; + }).join(''); + } catch (e) { + list.innerHTML = '
불러오기 실패
'; + } +} + +function closeExpenseLoadModal() { + document.getElementById('expense-load-modal').style.display = 'none'; + document.body.style.overflow = ''; +} + +async function loadExpenseData(approvalId) { + try { + const res = await fetch(`/api/admin/approvals/${approvalId}`, { + headers: { 'Accept': 'application/json' }, + }); + const json = await res.json(); + + if (!json.success || !json.data?.content) { + showToast('데이터를 불러올 수 없습니다.', 'error'); + return; + } + + const content = json.data.content; + const expenseEl = document.getElementById('expense-form-container'); + if (!expenseEl || !expenseEl._x_dataStack) return; + + const alpine = expenseEl._x_dataStack[0]; + const today = new Date().toISOString().slice(0, 10); + + // 폼 데이터 복사 (날짜는 오늘로 초기화) + alpine.formData.expense_type = content.expense_type || 'corporate_card'; + alpine.formData.tax_invoice = content.tax_invoice || 'normal'; + alpine.formData.write_date = today; + alpine.formData.approval_date = today; + alpine.formData.department = content.department || '경리부'; + alpine.formData.writer_name = content.writer_name || ''; + alpine.formData.attachment_memo = content.attachment_memo || ''; + alpine.formData.selected_card = content.selected_card || null; + alpine.formData.selected_account = content.selected_account || null; + + // 내역 항목 복사 (날짜는 오늘로) + if (content.items && content.items.length > 0) { + let keyCounter = alpine.formData.items.length + 100; + alpine.formData.items = content.items.map(item => ({ + _key: ++keyCounter, + date: today, + description: item.description || '', + amount: parseInt(item.amount) || 0, + vendor: item.vendor || '', + bank: item.bank || '', + account_no: item.account_no || '', + depositor: item.depositor || '', + remark: item.remark || '', + })); + } + + // 제목 설정 + const titleEl = document.getElementById('title'); + if (!titleEl.value.trim()) { + titleEl.value = json.data.title || '지출결의서'; + } + + closeExpenseLoadModal(); + showToast('지출결의서를 불러왔습니다. 내용을 확인 후 수정해주세요.', 'success'); + } catch (e) { + showToast('불러오기 실패', 'error'); + } +} + + @endpush diff --git a/routes/api.php b/routes/api.php index 38476164..e1457946 100644 --- a/routes/api.php +++ b/routes/api.php @@ -958,6 +958,7 @@ Route::put('/lines/{id}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'updateLine'])->name('lines.update'); Route::delete('/lines/{id}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'destroyLine'])->name('lines.destroy'); Route::get('/forms', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'forms'])->name('forms'); + Route::get('/expense-history', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'expenseHistory'])->name('expense-history'); Route::get('/badge-counts', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'badgeCounts'])->name('badge-counts'); Route::post('/upload-file', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'uploadFile'])->name('upload-file'); Route::delete('/files/{fileId}', [\App\Http\Controllers\Api\Admin\ApprovalApiController::class, 'deleteFile'])->name('delete-file');