feat: [결재] 양식 마이그레이션 12종 + 반려이력/재상신

- 재직/경력/위촉증명서, 사직서, 사용인감계, 위임장
- 이사회의사록, 견적서, 공문서, 연차사용촉진 1차/2차
- 지출결의서 body_template 고도화
- rejection_history, resubmit_count, drafter_read_at 컬럼
- Document-Approval 브릿지 연동 (linkable)
- 수신함 날짜 범위 필터 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 02:58:32 +09:00
parent 5e4cbc7742
commit 3d12687a2d
5 changed files with 166 additions and 1 deletions

View File

@@ -22,6 +22,8 @@ public function rules(): array
'sort_dir' => 'nullable|string|in:asc,desc', 'sort_dir' => 'nullable|string|in:asc,desc',
'per_page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1',
'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',
]; ];
} }
} }

View File

@@ -8,6 +8,7 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
/** /**
@@ -55,6 +56,8 @@ class Approval extends Model
'completed_at', 'completed_at',
'current_step', 'current_step',
'attachments', 'attachments',
'linkable_type',
'linkable_id',
'created_by', 'created_by',
'updated_by', 'updated_by',
'deleted_by', 'deleted_by',
@@ -135,6 +138,14 @@ public function referenceSteps(): HasMany
->orderBy('step_order'); ->orderBy('step_order');
} }
/**
* 연결 대상 (Document 등)
*/
public function linkable(): MorphTo
{
return $this->morphTo();
}
/** /**
* 생성자 * 생성자
*/ */

View File

@@ -2,6 +2,7 @@
namespace App\Services; namespace App\Services;
use App\Models\Documents\Document;
use App\Models\Tenants\Approval; use App\Models\Tenants\Approval;
use App\Models\Tenants\ApprovalForm; use App\Models\Tenants\ApprovalForm;
use App\Models\Tenants\ApprovalLine; 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'; $sortBy = $params['sort_by'] ?? 'created_at';
$sortDir = $params['sort_dir'] ?? 'desc'; $sortDir = $params['sort_dir'] ?? 'desc';
@@ -559,7 +568,7 @@ public function show(int $id): Approval
{ {
$tenantId = $this->tenantId(); $tenantId = $this->tenantId();
return Approval::query() $approval = Approval::query()
->where('tenant_id', $tenantId) ->where('tenant_id', $tenantId)
->with([ ->with([
'form:id,name,code,category,template', 'form:id,name,code,category,template',
@@ -571,6 +580,19 @@ public function show(int $id): Approval
'steps.approver.tenantProfile.department:id,name', 'steps.approver.tenantProfile.department:id,name',
]) ])
->findOrFail($id); ->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->updated_by = $userId;
$approval->save(); $approval->save();
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
return $approval->fresh([ return $approval->fresh([
'form:id,name,code,category', 'form:id,name,code,category',
'drafter:id,name', 'drafter:id,name',
@@ -887,6 +912,9 @@ public function reject(int $id, string $comment): Approval
$approval->updated_by = $userId; $approval->updated_by = $userId;
$approval->save(); $approval->save();
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
return $approval->fresh([ return $approval->fresh([
'form:id,name,code,category', 'form:id,name,code,category',
'drafter:id,name', 'drafter:id,name',
@@ -926,6 +954,9 @@ public function cancel(int $id): Approval
$approval->updated_by = $userId; $approval->updated_by = $userId;
$approval->save(); $approval->save();
// Document 브릿지 동기화 (steps 삭제 전에 실행)
$this->syncToLinkedDocument($approval);
// 결재 단계들 삭제 // 결재 단계들 삭제
$approval->steps()->delete(); $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,
]);
}
/** /**
* 참조 열람 처리 * 참조 열람 처리
*/ */

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 결재(approvals) 테이블에 연결 대상(linkable) 컬럼 추가
* - Document 시스템과 Approval 시스템을 브릿지하기 위한 다형성 관계
*/
public function up(): void
{
Schema::table('approvals', function (Blueprint $table) {
$table->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']);
});
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$tenants = DB::table('tenants')->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();
}
};