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

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Documents\Document;
use App\Models\Tenants\Approval;
use App\Models\Tenants\ApprovalForm;
use App\Models\Tenants\ApprovalLine;
@@ -567,7 +568,7 @@ public function show(int $id): Approval
{
$tenantId = $this->tenantId();
return Approval::query()
$approval = Approval::query()
->where('tenant_id', $tenantId)
->with([
'form:id,name,code,category,template',
@@ -579,6 +580,19 @@ public function show(int $id): Approval
'steps.approver.tenantProfile.department:id,name',
])
->findOrFail($id);
// Document 브릿지: 연결된 문서 데이터 로딩
if ($approval->linkable_type === Document::class) {
$approval->load([
'linkable.template',
'linkable.template.approvalLines',
'linkable.data',
'linkable.approvals.user:id,name',
'linkable.attachments',
]);
}
return $approval;
}
/**
@@ -842,6 +856,9 @@ public function approve(int $id, ?string $comment = null): Approval
$approval->updated_by = $userId;
$approval->save();
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
@@ -895,6 +912,9 @@ public function reject(int $id, string $comment): Approval
$approval->updated_by = $userId;
$approval->save();
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
@@ -934,6 +954,9 @@ public function cancel(int $id): Approval
$approval->updated_by = $userId;
$approval->save();
// Document 브릿지 동기화 (steps 삭제 전에 실행)
$this->syncToLinkedDocument($approval);
// 결재 단계들 삭제
$approval->steps()->delete();
@@ -944,6 +967,57 @@ public function cancel(int $id): Approval
});
}
/**
* Approval → Document 브릿지 동기화
* 결재 승인/반려/회수 시 연결된 Document의 상태와 결재란을 동기화
*/
private function syncToLinkedDocument(Approval $approval): void
{
if ($approval->linkable_type !== Document::class) {
return;
}
$document = Document::find($approval->linkable_id);
if (! $document) {
return;
}
// approval_steps → document_approvals 동기화 (승인자 이름/시각 반영)
foreach ($approval->steps as $step) {
if ($step->status === ApprovalStep::STATUS_PENDING) {
continue;
}
$docApproval = $document->approvals()
->where('step', $step->step_order)
->first();
if ($docApproval) {
$docApproval->update([
'status' => strtoupper($step->status),
'acted_at' => $step->acted_at,
'comment' => $step->comment,
]);
}
}
// Document 전체 상태 동기화
$documentStatus = match ($approval->status) {
Approval::STATUS_APPROVED => Document::STATUS_APPROVED,
Approval::STATUS_REJECTED => Document::STATUS_REJECTED,
Approval::STATUS_CANCELLED => Document::STATUS_CANCELLED,
default => Document::STATUS_PENDING,
};
$document->update([
'status' => $documentStatus,
'completed_at' => in_array($approval->status, [
Approval::STATUS_APPROVED,
Approval::STATUS_REJECTED,
]) ? now() : null,
]);
}
/**
* 참조 열람 처리
*/