'integer', 'storage_limit' => 'integer', 'storage_used' => 'integer', 'trial_ends_at' => 'datetime', 'expires_at' => 'datetime', 'last_paid_at' => 'datetime', 'storage_warning_sent_at' => 'datetime', 'storage_grace_period_until' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', ]; /** * 활성 테넌트만 조회 (삭제되지 않은 모든 테넌트) */ public function scopeActive($query) { return $query->whereNull('deleted_at'); } /** * 관계: 사용자 (Many-to-Many via user_tenants) */ public function users(): BelongsToMany { return $this->belongsToMany(User::class, 'user_tenants'); } /** * 관계: 부서 */ public function departments(): HasMany { return $this->hasMany(\App\Models\Tenants\Department::class, 'tenant_id'); } /** * 관계: 메뉴 */ public function menus(): HasMany { return $this->hasMany(\App\Models\Commons\Menu::class, 'tenant_id'); } /** * 관계: 역할 */ public function roles(): HasMany { return $this->hasMany(\App\Models\Permissions\Role::class, 'tenant_id'); } /** * 관계: 삭제한 사용자 */ public function deletedByUser(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(User::class, 'deleted_by'); } /** * 상태 배지 색상 (Blade 뷰에서 사용) */ public function getStatusBadgeColorAttribute(): string { return match ($this->tenant_st_code) { 'active' => 'success', 'trial' => 'warning', 'suspended', 'expired' => 'error', default => 'neutral', }; } /** * 상태 한글명 (Blade 뷰에서 사용) */ public function getStatusLabelAttribute(): string { return match ($this->tenant_st_code) { 'trial' => '트라이얼', 'active' => '활성', 'suspended' => '정지', 'expired' => '만료', default => $this->tenant_st_code ?? '미설정', }; } /** * 결제 유형 한글명 (Blade 뷰에서 사용) */ public function getBillingTypeLabelAttribute(): ?string { if (! $this->billing_tp_code) { return null; } return match ($this->billing_tp_code) { 'monthly' => '월간', 'yearly' => '연간', 'free' => '무료', default => $this->billing_tp_code, }; } /** * 저장소 사용률 (%) - Blade 뷰에서 사용 */ public function getStorageUsagePercentAttribute(): float { $limit = $this->storage_limit ?? 10737418240; // 기본 10GB if ($limit <= 0) { return 0; } return round(($this->storage_used ?? 0) / $limit * 100, 1); } /** * 저장소 사용량 포맷 (예: "2.5 GB / 10 GB") */ public function getStorageUsageFormattedAttribute(): string { $used = $this->formatBytes($this->storage_used ?? 0); $limit = $this->formatBytes($this->storage_limit ?? 10737418240); return "{$used} / {$limit}"; } /** * 저장소 사용량만 포맷 (예: "2.5 GB") */ public function getStorageUsedFormattedAttribute(): string { return $this->formatBytes($this->storage_used ?? 0); } /** * 저장소 한도만 포맷 (예: "10 GB") */ public function getStorageLimitFormattedAttribute(): string { return $this->formatBytes($this->storage_limit ?? 10737418240); } /** * 저장소 상태 배지 색상 (Blade 뷰에서 사용) */ public function getStorageBadgeColorAttribute(): string { $percent = $this->storage_usage_percent; return match (true) { $percent >= 90 => 'error', $percent >= 70 => 'warning', default => 'success', }; } /** * 바이트를 읽기 쉬운 형식으로 변환 */ protected function formatBytes(int $bytes): string { if ($bytes <= 0) { return '0 B'; } $units = ['B', 'KB', 'MB', 'GB', 'TB']; $factor = floor(log($bytes, 1024)); $factor = min($factor, count($units) - 1); return round($bytes / pow(1024, $factor), 1).' '.$units[$factor]; } /** * 전화번호 포맷 (하이픈 추가) */ public function getPhoneFormattedAttribute(): ?string { if (! $this->phone) { return null; } // 숫자만 추출 $numbers = preg_replace('/[^0-9]/', '', $this->phone); // 휴대폰 (010, 011, 016, 017, 018, 019) if (preg_match('/^(01[0-9])(\d{3,4})(\d{4})$/', $numbers, $matches)) { return $matches[1].'-'.$matches[2].'-'.$matches[3]; } // 서울 (02) if (preg_match('/^(02)(\d{3,4})(\d{4})$/', $numbers, $matches)) { return $matches[1].'-'.$matches[2].'-'.$matches[3]; } // 지역번호 (031, 032, ...) if (preg_match('/^(0\d{2})(\d{3,4})(\d{4})$/', $numbers, $matches)) { return $matches[1].'-'.$matches[2].'-'.$matches[3]; } // 대표번호 (1588, 1544, ...) if (preg_match('/^(1\d{3})(\d{4})$/', $numbers, $matches)) { return $matches[1].'-'.$matches[2]; } // 포맷 불가 시 원본 반환 return $this->phone; } }