diff --git a/app/Models/Tenants/Approval.php b/app/Models/Tenants/Approval.php index 9ecc871..373e14f 100644 --- a/app/Models/Tenants/Approval.php +++ b/app/Models/Tenants/Approval.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\SoftDeletes; /** @@ -55,6 +56,8 @@ class Approval extends Model 'completed_at', 'current_step', 'attachments', + 'linkable_type', + 'linkable_id', 'created_by', 'updated_by', 'deleted_by', @@ -135,6 +138,14 @@ public function referenceSteps(): HasMany ->orderBy('step_order'); } + /** + * 연결 대상 (Document 등) + */ + public function linkable(): MorphTo + { + return $this->morphTo(); + } + /** * 생성자 */ diff --git a/app/Services/ApprovalService.php b/app/Services/ApprovalService.php index 4e4ce2a..46ebf16 100644 --- a/app/Services/ApprovalService.php +++ b/app/Services/ApprovalService.php @@ -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, + ]); + } + /** * 참조 열람 처리 */ diff --git a/app/Services/DocumentService.php b/app/Services/DocumentService.php index b4e614c..b939fe9 100644 --- a/app/Services/DocumentService.php +++ b/app/Services/DocumentService.php @@ -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, + ]); + } + } + /** * 결재 승인 */ diff --git a/database/migrations/2026_02_27_200000_add_linkable_to_approvals_table.php b/database/migrations/2026_02_27_200000_add_linkable_to_approvals_table.php new file mode 100644 index 0000000..32901fb --- /dev/null +++ b/database/migrations/2026_02_27_200000_add_linkable_to_approvals_table.php @@ -0,0 +1,32 @@ +string('linkable_type')->nullable()->after('attachments')->comment('연결 대상 모델 (예: App\\Models\\Documents\\Document)'); + $table->unsignedBigInteger('linkable_id')->nullable()->after('linkable_type')->comment('연결 대상 ID'); + $table->index(['linkable_type', 'linkable_id'], 'idx_linkable'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('approvals', function (Blueprint $table) { + $table->dropIndex('idx_linkable'); + $table->dropColumn(['linkable_type', 'linkable_id']); + }); + } +};