feat: [approval] Document ↔ Approval 브릿지 연동 (Phase 4.2)

- Approval 모델에 linkable morphTo 관계 추가
- DocumentService: 상신 시 Approval 자동 생성 + approval_steps 변환
- ApprovalService: 승인/반려/회수 시 Document 상태 동기화
- approvals 테이블에 linkable_type, linkable_id 컬럼 마이그레이션

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 11:00:53 +09:00
parent ef7d9fae24
commit cd847e01a0
4 changed files with 197 additions and 1 deletions

View File

@@ -7,6 +7,10 @@
use App\Models\Documents\DocumentAttachment;
use App\Models\Documents\DocumentData;
use App\Models\Documents\DocumentTemplate;
use App\Models\Tenants\Approval;
use App\Models\Tenants\ApprovalForm;
use App\Models\Tenants\ApprovalLine;
use App\Models\Tenants\ApprovalStep;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -275,6 +279,9 @@ public function submit(int $id): Document
$document->updated_by = $userId;
$document->save();
// Approval 시스템 브릿지: 결재함(/approval/inbox)에 표시되도록 Approval 자동 생성
$this->createApprovalBridge($document);
return $document->fresh([
'template:id,name,category',
'approvals.user:id,name',
@@ -283,6 +290,78 @@ public function submit(int $id): Document
});
}
/**
* Document → Approval 브릿지 생성
* Document 상신 시 Approval 레코드를 자동 생성하여 /approval/inbox에 표시
*/
private function createApprovalBridge(Document $document): void
{
$form = ApprovalForm::where('code', 'document')
->where('tenant_id', $document->tenant_id)
->first();
if (! $form) {
return; // 문서 결재 양식 미등록 시 스킵 (기존 동작 유지)
}
// 기존 브릿지가 있으면 스킵 (재상신 방지)
$existingApproval = Approval::where('linkable_type', Document::class)
->where('linkable_id', $document->id)
->whereNotIn('status', [Approval::STATUS_CANCELLED])
->first();
if ($existingApproval) {
return;
}
// 문서번호 생성 (Approval 체계)
$today = now()->format('Ymd');
$lastNumber = Approval::where('tenant_id', $document->tenant_id)
->where('document_number', 'like', "AP-{$today}-%")
->orderByDesc('document_number')
->value('document_number');
$seq = 1;
if ($lastNumber && preg_match('/AP-\d{8}-(\d{4})/', $lastNumber, $matches)) {
$seq = (int) $matches[1] + 1;
}
$documentNumber = sprintf('AP-%s-%04d', $today, $seq);
$approval = Approval::create([
'tenant_id' => $document->tenant_id,
'document_number' => $documentNumber,
'form_id' => $form->id,
'title' => $document->title,
'content' => [
'document_id' => $document->id,
'template_id' => $document->template_id,
'document_no' => $document->document_no,
],
'status' => Approval::STATUS_PENDING,
'drafter_id' => $document->created_by,
'drafted_at' => now(),
'current_step' => 1,
'linkable_type' => Document::class,
'linkable_id' => $document->id,
'created_by' => $document->updated_by ?? $document->created_by,
]);
// document_approvals → approval_steps 변환
$docApprovals = $document->approvals()
->orderBy('step')
->get();
foreach ($docApprovals as $docApproval) {
ApprovalStep::create([
'approval_id' => $approval->id,
'step_order' => $docApproval->step,
'step_type' => ApprovalLine::STEP_TYPE_APPROVAL,
'approver_id' => $docApproval->user_id,
'status' => ApprovalStep::STATUS_PENDING,
]);
}
}
/**
* 결재 승인
*/