Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-02-12 14:16:32 +09:00
24 changed files with 1709 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models\ESign;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class EsignAuditLog extends Model
{
use BelongsToTenant;
protected $table = 'esign_audit_logs';
public $timestamps = false;
// 액션 상수
public const ACTION_CREATED = 'created';
public const ACTION_SENT = 'sent';
public const ACTION_VIEWED = 'viewed';
public const ACTION_OTP_SENT = 'otp_sent';
public const ACTION_AUTHENTICATED = 'authenticated';
public const ACTION_SIGNED = 'signed';
public const ACTION_REJECTED = 'rejected';
public const ACTION_COMPLETED = 'completed';
public const ACTION_CANCELLED = 'cancelled';
public const ACTION_REMINDED = 'reminded';
public const ACTION_DOWNLOADED = 'downloaded';
protected $fillable = [
'tenant_id',
'contract_id',
'signer_id',
'action',
'ip_address',
'user_agent',
'metadata',
'created_at',
];
protected $casts = [
'metadata' => 'array',
'created_at' => 'datetime',
];
// === Relations ===
public function contract(): BelongsTo
{
return $this->belongsTo(EsignContract::class, 'contract_id');
}
public function signer(): BelongsTo
{
return $this->belongsTo(EsignSigner::class, 'signer_id');
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Models\ESign;
use App\Models\Members\User;
use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class EsignContract extends Model
{
use Auditable, BelongsToTenant, SoftDeletes;
protected $table = 'esign_contracts';
// 상태 상수
public const STATUS_DRAFT = 'draft';
public const STATUS_PENDING = 'pending';
public const STATUS_PARTIALLY_SIGNED = 'partially_signed';
public const STATUS_COMPLETED = 'completed';
public const STATUS_EXPIRED = 'expired';
public const STATUS_CANCELLED = 'cancelled';
public const STATUS_REJECTED = 'rejected';
public const STATUSES = [
self::STATUS_DRAFT,
self::STATUS_PENDING,
self::STATUS_PARTIALLY_SIGNED,
self::STATUS_COMPLETED,
self::STATUS_EXPIRED,
self::STATUS_CANCELLED,
self::STATUS_REJECTED,
];
// 서명 순서 상수
public const SIGN_ORDER_COUNTERPART_FIRST = 'counterpart_first';
public const SIGN_ORDER_CREATOR_FIRST = 'creator_first';
public const SIGN_ORDERS = [
self::SIGN_ORDER_COUNTERPART_FIRST,
self::SIGN_ORDER_CREATOR_FIRST,
];
protected $fillable = [
'tenant_id',
'contract_code',
'title',
'description',
'sign_order_type',
'original_file_path',
'original_file_name',
'original_file_hash',
'original_file_size',
'signed_file_path',
'signed_file_hash',
'status',
'expires_at',
'completed_at',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'original_file_size' => 'integer',
'expires_at' => 'datetime',
'completed_at' => 'datetime',
];
// === Relations ===
public function signers(): HasMany
{
return $this->hasMany(EsignSigner::class, 'contract_id');
}
public function signFields(): HasMany
{
return $this->hasMany(EsignSignField::class, 'contract_id');
}
public function auditLogs(): HasMany
{
return $this->hasMany(EsignAuditLog::class, 'contract_id');
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
// === Scopes ===
public function scopeStatus($query, string $status)
{
return $query->where('status', $status);
}
public function scopeActive($query)
{
return $query->whereNotIn('status', [
self::STATUS_CANCELLED,
self::STATUS_EXPIRED,
self::STATUS_REJECTED,
]);
}
// === Helpers ===
public function isExpired(): bool
{
return $this->expires_at && $this->expires_at->isPast();
}
public function canSign(): bool
{
return in_array($this->status, [
self::STATUS_PENDING,
self::STATUS_PARTIALLY_SIGNED,
]) && ! $this->isExpired();
}
public function getNextSigner(): ?EsignSigner
{
return $this->signers()
->whereIn('status', [EsignSigner::STATUS_WAITING, EsignSigner::STATUS_NOTIFIED, EsignSigner::STATUS_AUTHENTICATED])
->orderBy('sign_order')
->first();
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Models\ESign;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class EsignSignField extends Model
{
use BelongsToTenant;
protected $table = 'esign_sign_fields';
// 필드 유형 상수
public const TYPE_SIGNATURE = 'signature';
public const TYPE_STAMP = 'stamp';
public const TYPE_TEXT = 'text';
public const TYPE_DATE = 'date';
public const TYPE_CHECKBOX = 'checkbox';
public const FIELD_TYPES = [
self::TYPE_SIGNATURE,
self::TYPE_STAMP,
self::TYPE_TEXT,
self::TYPE_DATE,
self::TYPE_CHECKBOX,
];
protected $fillable = [
'tenant_id',
'contract_id',
'signer_id',
'page_number',
'position_x',
'position_y',
'width',
'height',
'field_type',
'field_label',
'field_value',
'is_required',
'sort_order',
];
protected $casts = [
'page_number' => 'integer',
'position_x' => 'decimal:2',
'position_y' => 'decimal:2',
'width' => 'decimal:2',
'height' => 'decimal:2',
'is_required' => 'boolean',
'sort_order' => 'integer',
];
// === Relations ===
public function contract(): BelongsTo
{
return $this->belongsTo(EsignContract::class, 'contract_id');
}
public function signer(): BelongsTo
{
return $this->belongsTo(EsignSigner::class, 'signer_id');
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Models\ESign;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class EsignSigner extends Model
{
use BelongsToTenant;
protected $table = 'esign_signers';
// 역할 상수
public const ROLE_CREATOR = 'creator';
public const ROLE_COUNTERPART = 'counterpart';
// 상태 상수
public const STATUS_WAITING = 'waiting';
public const STATUS_NOTIFIED = 'notified';
public const STATUS_AUTHENTICATED = 'authenticated';
public const STATUS_SIGNED = 'signed';
public const STATUS_REJECTED = 'rejected';
public const STATUSES = [
self::STATUS_WAITING,
self::STATUS_NOTIFIED,
self::STATUS_AUTHENTICATED,
self::STATUS_SIGNED,
self::STATUS_REJECTED,
];
protected $fillable = [
'tenant_id',
'contract_id',
'role',
'sign_order',
'name',
'email',
'phone',
'access_token',
'token_expires_at',
'otp_code',
'otp_expires_at',
'otp_attempts',
'auth_verified_at',
'signature_image_path',
'signed_at',
'consent_agreed_at',
'sign_ip_address',
'sign_user_agent',
'status',
'rejected_reason',
];
protected $casts = [
'sign_order' => 'integer',
'otp_attempts' => 'integer',
'token_expires_at' => 'datetime',
'otp_expires_at' => 'datetime',
'auth_verified_at' => 'datetime',
'signed_at' => 'datetime',
'consent_agreed_at' => 'datetime',
];
protected $hidden = [
'access_token',
'otp_code',
];
// === Relations ===
public function contract(): BelongsTo
{
return $this->belongsTo(EsignContract::class, 'contract_id');
}
public function signFields(): HasMany
{
return $this->hasMany(EsignSignField::class, 'signer_id');
}
// === Helpers ===
public function isVerified(): bool
{
return $this->auth_verified_at !== null;
}
public function hasSigned(): bool
{
return $this->signed_at !== null;
}
public function canSign(): bool
{
return $this->isVerified()
&& ! $this->hasSigned()
&& $this->status !== self::STATUS_REJECTED
&& $this->contract->canSign();
}
}