From 3d12687a2dc1d8c7e48b25e57591b20100dc234b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 7 Mar 2026 02:58:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EA=B2=B0=EC=9E=AC]=20=EC=96=91?= =?UTF-8?q?=EC=8B=9D=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=2012=EC=A2=85=20+=20=EB=B0=98=EB=A0=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5/=EC=9E=AC=EC=83=81=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 재직/경력/위촉증명서, 사직서, 사용인감계, 위임장 - 이사회의사록, 견적서, 공문서, 연차사용촉진 1차/2차 - 지출결의서 body_template 고도화 - rejection_history, resubmit_count, drafter_read_at 컬럼 - Document-Approval 브릿지 연동 (linkable) - 수신함 날짜 범위 필터 추가 Co-Authored-By: Claude Opus 4.6 --- .../Requests/Approval/InboxIndexRequest.php | 2 + app/Models/Tenants/Approval.php | 11 +++ app/Services/ApprovalService.php | 84 ++++++++++++++++++- ...200000_add_linkable_to_approvals_table.php | 32 +++++++ ...2026_03_06_100000_add_resignation_form.php | 38 +++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_02_27_200000_add_linkable_to_approvals_table.php create mode 100644 database/migrations/2026_03_06_100000_add_resignation_form.php diff --git a/app/Http/Requests/Approval/InboxIndexRequest.php b/app/Http/Requests/Approval/InboxIndexRequest.php index b6d7ac0..702bb65 100644 --- a/app/Http/Requests/Approval/InboxIndexRequest.php +++ b/app/Http/Requests/Approval/InboxIndexRequest.php @@ -22,6 +22,8 @@ public function rules(): array 'sort_dir' => 'nullable|string|in:asc,desc', 'per_page' => 'nullable|integer|min:1', 'page' => 'nullable|integer|min:1', + 'start_date' => 'nullable|date_format:Y-m-d', + 'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date', ]; } } 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 7516427..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; @@ -446,6 +447,14 @@ public function inbox(array $params): LengthAwarePaginator } } + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->whereDate('created_at', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->whereDate('created_at', '<=', $params['end_date']); + } + // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; @@ -559,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', @@ -571,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; } /** @@ -834,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', @@ -887,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', @@ -926,6 +954,9 @@ public function cancel(int $id): Approval $approval->updated_by = $userId; $approval->save(); + // Document 브릿지 동기화 (steps 삭제 전에 실행) + $this->syncToLinkedDocument($approval); + // 결재 단계들 삭제 $approval->steps()->delete(); @@ -936,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/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']); + }); + } +}; diff --git a/database/migrations/2026_03_06_100000_add_resignation_form.php b/database/migrations/2026_03_06_100000_add_resignation_form.php new file mode 100644 index 0000000..680d681 --- /dev/null +++ b/database/migrations/2026_03_06_100000_add_resignation_form.php @@ -0,0 +1,38 @@ +whereNull('deleted_at')->pluck('id'); + + foreach ($tenants as $tenantId) { + $exists = DB::table('approval_forms') + ->where('tenant_id', $tenantId) + ->where('code', 'resignation') + ->exists(); + + if (! $exists) { + DB::table('approval_forms')->insert([ + 'tenant_id' => $tenantId, + 'name' => '사직서', + 'code' => 'resignation', + 'category' => 'certificate', + 'template' => '[]', + 'body_template' => '', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + } + + public function down(): void + { + DB::table('approval_forms')->where('code', 'resignation')->delete(); + } +};