From 5111db24c291b657f4fd73d87f841ec63774242a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 31 Jan 2026 05:00:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EA=B2=B0=EC=9E=AC=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EA=B5=AC=ED=98=84=20(Phase=202.3?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API: submit(DRAFT→PENDING), approve(단계별 승인), reject(반려 사유 필수) - 전체 승인 완료 시 자동 APPROVED, 재제출 시 결재라인 초기화 - edit: 결재 제출 버튼 + submitForApproval() JS - show: 승인/반려 버튼, 반려 사유 모달, 결재 현황 속성 수정, 상태 배지 CSS - 라우트: submit/approve/reject 3개 추가 Co-Authored-By: Claude Opus 4.5 --- .../Api/Admin/DocumentApiController.php | 145 +++++++++++++++++ resources/views/documents/edit.blade.php | 35 ++++- resources/views/documents/show.blade.php | 146 ++++++++++++++++-- routes/api.php | 4 + 4 files changed, 320 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Api/Admin/DocumentApiController.php b/app/Http/Controllers/Api/Admin/DocumentApiController.php index 5f61a910..e5ee7d5a 100644 --- a/app/Http/Controllers/Api/Admin/DocumentApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentApiController.php @@ -215,6 +215,151 @@ public function destroy(int $id): JsonResponse ]); } + /** + * 결재 제출 (DRAFT → PENDING) + */ + public function submit(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id'); + $userId = auth()->id(); + + $document = Document::where('tenant_id', $tenantId)->findOrFail($id); + + if ($document->status !== Document::STATUS_DRAFT && $document->status !== Document::STATUS_REJECTED) { + return response()->json([ + 'success' => false, + 'message' => '작성중 또는 반려 상태의 문서만 제출할 수 있습니다.', + ], 422); + } + + $document->update([ + 'status' => Document::STATUS_PENDING, + 'submitted_at' => now(), + 'updated_by' => $userId, + ]); + + // 결재라인 상태 초기화 (반려 후 재제출 시) + $document->approvals()->update([ + 'status' => DocumentApproval::STATUS_PENDING, + 'comment' => null, + 'acted_at' => null, + ]); + + return response()->json([ + 'success' => true, + 'message' => '결재가 제출되었습니다.', + 'data' => $document->fresh(['approvals']), + ]); + } + + /** + * 결재 승인 (단계별) + */ + public function approve(Request $request, int $id): JsonResponse + { + $tenantId = session('selected_tenant_id'); + $userId = auth()->id(); + + $document = Document::where('tenant_id', $tenantId)->findOrFail($id); + + if ($document->status !== Document::STATUS_PENDING) { + return response()->json([ + 'success' => false, + 'message' => '결재중 상태의 문서만 승인할 수 있습니다.', + ], 422); + } + + // 현재 단계의 미처리 결재 찾기 + $pendingApproval = $document->approvals() + ->where('status', DocumentApproval::STATUS_PENDING) + ->orderBy('step') + ->first(); + + if (! $pendingApproval) { + return response()->json([ + 'success' => false, + 'message' => '승인 대기 중인 결재 단계가 없습니다.', + ], 422); + } + + $pendingApproval->update([ + 'user_id' => $userId, + 'status' => DocumentApproval::STATUS_APPROVED, + 'comment' => $request->input('comment'), + 'acted_at' => now(), + 'updated_by' => $userId, + ]); + + // 모든 결재가 완료되었는지 확인 + $remainingPending = $document->approvals() + ->where('status', DocumentApproval::STATUS_PENDING) + ->count(); + + if ($remainingPending === 0) { + $document->update([ + 'status' => Document::STATUS_APPROVED, + 'completed_at' => now(), + 'updated_by' => $userId, + ]); + } + + return response()->json([ + 'success' => true, + 'message' => $remainingPending === 0 ? '최종 승인되었습니다.' : '승인되었습니다. (다음 단계 대기)', + 'data' => $document->fresh(['approvals']), + ]); + } + + /** + * 결재 반려 + */ + public function reject(Request $request, int $id): JsonResponse + { + $tenantId = session('selected_tenant_id'); + $userId = auth()->id(); + + $request->validate([ + 'comment' => 'required|string|max:500', + ]); + + $document = Document::where('tenant_id', $tenantId)->findOrFail($id); + + if ($document->status !== Document::STATUS_PENDING) { + return response()->json([ + 'success' => false, + 'message' => '결재중 상태의 문서만 반려할 수 있습니다.', + ], 422); + } + + // 현재 단계 결재에 반려 기록 + $pendingApproval = $document->approvals() + ->where('status', DocumentApproval::STATUS_PENDING) + ->orderBy('step') + ->first(); + + if ($pendingApproval) { + $pendingApproval->update([ + 'user_id' => $userId, + 'status' => DocumentApproval::STATUS_REJECTED, + 'comment' => $request->input('comment'), + 'acted_at' => now(), + 'updated_by' => $userId, + ]); + } + + $document->update([ + 'status' => Document::STATUS_REJECTED, + 'completed_at' => now(), + 'updated_by' => $userId, + ]); + + return response()->json([ + 'success' => true, + 'message' => '문서가 반려되었습니다.', + 'data' => $document->fresh(['approvals']), + ]); + } + /** * 문서 데이터 저장 (기본필드 + 섹션 테이블 데이터) */ diff --git a/resources/views/documents/edit.blade.php b/resources/views/documents/edit.blade.php index 33e07518..96c9af86 100644 --- a/resources/views/documents/edit.blade.php +++ b/resources/views/documents/edit.blade.php @@ -320,8 +320,14 @@ class="px-6 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover: + @if(!$isCreate && $document && $document->canEdit()) + + @endif @endif @@ -418,5 +424,32 @@ class="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg- }); }); }); + +@if(!$isCreate && $document) +window.submitForApproval = function() { + if (!confirm('결재를 제출하시겠습니까? 제출 후에는 수정이 불가합니다.')) return; + + fetch('/api/admin/documents/{{ $document->id }}/submit', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, + } + }) + .then(response => response.json()) + .then(result => { + if (result.success) { + showToast(result.message, 'success'); + window.location.href = '/documents/{{ $document->id }}'; + } else { + showToast(result.message || '오류가 발생했습니다.', 'error'); + } + }) + .catch(error => { + console.error('Submit error:', error); + showToast('결재 제출 중 오류가 발생했습니다.', 'error'); + }); +}; +@endif @endpush \ No newline at end of file diff --git a/resources/views/documents/show.blade.php b/resources/views/documents/show.blade.php index 5291b174..f5335e13 100644 --- a/resources/views/documents/show.blade.php +++ b/resources/views/documents/show.blade.php @@ -10,7 +10,7 @@ -@endsection \ No newline at end of file +{{-- 반려 사유 모달 --}} +@if($document->isPending()) + +@endif + +@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 2fafeb3a..b91e5c5e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -803,6 +803,10 @@ Route::get('/{id}', [DocumentApiController::class, 'show'])->name('show'); Route::patch('/{id}', [DocumentApiController::class, 'update'])->name('update'); Route::delete('/{id}', [DocumentApiController::class, 'destroy'])->name('destroy'); + // 결재 워크플로우 + Route::post('/{id}/submit', [DocumentApiController::class, 'submit'])->name('submit'); + Route::post('/{id}/approve', [DocumentApiController::class, 'approve'])->name('approve'); + Route::post('/{id}/reject', [DocumentApiController::class, 'reject'])->name('reject'); }); /*