- Auditable 트레이트 신규 생성 (bootAuditable 패턴) - creating: created_by/updated_by 자동 채우기 - updating: updated_by 자동 채우기 - deleting: deleted_by 채우기 + saveQuietly() - created/updated/deleted: audit_logs 자동 기록 - 기존 AuditLogger 패턴과 동일한 try/catch 조용한 실패 - 변경된 필드만 before/after 기록 (updated 이벤트) - auditExclude 프로퍼티로 모델별 제외 필드 설정 가능 - 제외 대상: Attendance, StockTransaction, TodayIssue 등 고빈도/시스템 모델 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
178 lines
3.7 KiB
PHP
178 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Models\Members\User;
|
|
use App\Models\Permissions\Role;
|
|
use App\Models\Tenants\Tenant;
|
|
use App\Traits\Auditable;
|
|
use App\Traits\BelongsToTenant;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Support\Str;
|
|
|
|
class UserInvitation extends Model
|
|
{
|
|
use Auditable, BelongsToTenant;
|
|
|
|
// 상태 상수
|
|
public const STATUS_PENDING = 'pending';
|
|
|
|
public const STATUS_ACCEPTED = 'accepted';
|
|
|
|
public const STATUS_EXPIRED = 'expired';
|
|
|
|
public const STATUS_CANCELLED = 'cancelled';
|
|
|
|
// 기본 만료 기간 (일)
|
|
public const DEFAULT_EXPIRES_DAYS = 7;
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'email',
|
|
'role_id',
|
|
'message',
|
|
'token',
|
|
'status',
|
|
'invited_by',
|
|
'expires_at',
|
|
'accepted_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'expires_at' => 'datetime',
|
|
'accepted_at' => 'datetime',
|
|
];
|
|
|
|
/**
|
|
* 테넌트 관계
|
|
*/
|
|
public function tenant(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Tenant::class);
|
|
}
|
|
|
|
/**
|
|
* 역할 관계
|
|
*/
|
|
public function role(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Role::class);
|
|
}
|
|
|
|
/**
|
|
* 초대한 사용자 관계
|
|
*/
|
|
public function inviter(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'invited_by');
|
|
}
|
|
|
|
/**
|
|
* 토큰 생성
|
|
*/
|
|
public static function generateToken(): string
|
|
{
|
|
return Str::random(64);
|
|
}
|
|
|
|
/**
|
|
* 만료 일시 계산
|
|
*/
|
|
public static function calculateExpiresAt(int $days = self::DEFAULT_EXPIRES_DAYS): \DateTime
|
|
{
|
|
return now()->addDays($days);
|
|
}
|
|
|
|
/**
|
|
* 만료 여부 확인
|
|
*/
|
|
public function isExpired(): bool
|
|
{
|
|
return $this->expires_at->isPast();
|
|
}
|
|
|
|
/**
|
|
* 수락 가능 여부 확인
|
|
*/
|
|
public function canAccept(): bool
|
|
{
|
|
return $this->status === self::STATUS_PENDING && ! $this->isExpired();
|
|
}
|
|
|
|
/**
|
|
* 취소 가능 여부 확인
|
|
*/
|
|
public function canCancel(): bool
|
|
{
|
|
return $this->status === self::STATUS_PENDING;
|
|
}
|
|
|
|
/**
|
|
* 초대 수락 처리
|
|
*/
|
|
public function markAsAccepted(): void
|
|
{
|
|
$this->status = self::STATUS_ACCEPTED;
|
|
$this->accepted_at = now();
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* 초대 만료 처리
|
|
*/
|
|
public function markAsExpired(): void
|
|
{
|
|
$this->status = self::STATUS_EXPIRED;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* 초대 취소 처리
|
|
*/
|
|
public function markAsCancelled(): void
|
|
{
|
|
$this->status = self::STATUS_CANCELLED;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Scope: 대기 중인 초대만
|
|
*/
|
|
public function scopePending($query)
|
|
{
|
|
return $query->where('status', self::STATUS_PENDING);
|
|
}
|
|
|
|
/**
|
|
* Scope: 만료된 초대 (상태 업데이트 대상)
|
|
*/
|
|
public function scopeExpiredPending($query)
|
|
{
|
|
return $query->where('status', self::STATUS_PENDING)
|
|
->where('expires_at', '<', now());
|
|
}
|
|
|
|
/**
|
|
* Scope: 특정 이메일 초대
|
|
*/
|
|
public function scopeForEmail($query, string $email)
|
|
{
|
|
return $query->where('email', $email);
|
|
}
|
|
|
|
/**
|
|
* 상태 라벨 반환
|
|
*/
|
|
public function getStatusLabelAttribute(): string
|
|
{
|
|
return match ($this->status) {
|
|
self::STATUS_PENDING => '대기중',
|
|
self::STATUS_ACCEPTED => '수락됨',
|
|
self::STATUS_EXPIRED => '만료됨',
|
|
self::STATUS_CANCELLED => '취소됨',
|
|
default => $this->status,
|
|
};
|
|
}
|
|
}
|