feat:결재 워크플로우 구현 (Phase 2.3)

- API: submit(DRAFT→PENDING), approve(단계별 승인), reject(반려 사유 필수)
- 전체 승인 완료 시 자동 APPROVED, 재제출 시 결재라인 초기화
- edit: 결재 제출 버튼 + submitForApproval() JS
- show: 승인/반려 버튼, 반려 사유 모달, 결재 현황 속성 수정, 상태 배지 CSS
- 라우트: submit/approve/reject 3개 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 05:00:48 +09:00
parent d43f8d0ba1
commit 5111db24c2
4 changed files with 320 additions and 10 deletions

View File

@@ -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']),
]);
}
/**
* 문서 데이터 저장 (기본필드 + 섹션 테이블 데이터)
*/