diff --git a/app/Models/Tenants/Tenant.php b/app/Models/Tenants/Tenant.php index 282d41f..7f33967 100644 --- a/app/Models/Tenants/Tenant.php +++ b/app/Models/Tenants/Tenant.php @@ -18,6 +18,32 @@ class Tenant extends Model { use ModelTrait, SoftDeletes; + // 데모 테넌트 유형 상수 + const TYPE_STD = 'STD'; + + const TYPE_TPL = 'TPL'; + + const TYPE_HQ = 'HQ'; + + const TYPE_DEMO_SHOWCASE = 'DEMO_SHOWCASE'; + + const TYPE_DEMO_PARTNER = 'DEMO_PARTNER'; + + const TYPE_DEMO_TRIAL = 'DEMO_TRIAL'; + + const DEMO_TYPES = [ + self::TYPE_DEMO_SHOWCASE, + self::TYPE_DEMO_PARTNER, + self::TYPE_DEMO_TRIAL, + ]; + + // 데모 options 키 상수 + const OPTION_DEMO_PRESET = 'demo_preset'; + + const OPTION_DEMO_LIMITS = 'demo_limits'; + + const OPTION_DEMO_READ_ONLY = 'demo_read_only'; + protected $fillable = [ 'company_name', 'code', @@ -34,6 +60,9 @@ class Tenant extends Model 'options', 'tenant_st_code', 'billing_tp_code', + 'tenant_type', + 'demo_expires_at', + 'demo_source_partner_id', ]; protected $guarded = [ @@ -47,6 +76,7 @@ class Tenant extends Model protected $casts = [ 'trial_ends_at' => 'datetime', + 'demo_expires_at' => 'datetime', 'expires_at' => 'datetime', 'last_paid_at' => 'datetime', 'max_users' => 'integer', @@ -244,4 +274,132 @@ private function formatBytes(int $bytes): string return round($bytes, 2).' '.$units[$pow]; } + + // ─── options 헬퍼 메서드 ─── + + public function getOption(string $key, mixed $default = null): mixed + { + return data_get($this->options, $key, $default); + } + + public function setOption(string $key, mixed $value): self + { + $options = $this->options ?? []; + data_set($options, $key, $value); + $this->options = $options; + + return $this; + } + + // ─── 데모 테넌트 관련 메서드 ─── + + /** + * 데모 테넌트인지 확인 (모든 데모 유형) + */ + public function isDemoTenant(): bool + { + return in_array($this->tenant_type, self::DEMO_TYPES); + } + + /** + * 데모 쇼케이스(공용 읽기전용)인지 확인 + */ + public function isDemoShowcase(): bool + { + return $this->tenant_type === self::TYPE_DEMO_SHOWCASE; + } + + /** + * 파트너 데모인지 확인 + */ + public function isDemoPartner(): bool + { + return $this->tenant_type === self::TYPE_DEMO_PARTNER; + } + + /** + * 고객 체험 데모인지 확인 + */ + public function isDemoTrial(): bool + { + return $this->tenant_type === self::TYPE_DEMO_TRIAL; + } + + /** + * 데모 만료 여부 확인 + */ + public function isDemoExpired(): bool + { + if (! $this->isDemoTenant()) { + return false; + } + + // 쇼케이스는 만료 없음 + if ($this->isDemoShowcase()) { + return false; + } + + return $this->demo_expires_at && now()->gt($this->demo_expires_at); + } + + /** + * 데모 읽기전용인지 확인 + */ + public function isDemoReadOnly(): bool + { + return $this->isDemoShowcase() + || $this->getOption(self::OPTION_DEMO_READ_ONLY, false); + } + + /** + * 데모 테넌트만 조회하는 스코프 + */ + public function scopeDemo($query) + { + return $query->whereIn('tenant_type', self::DEMO_TYPES); + } + + /** + * 데모 프리셋 조회 + */ + public function getDemoPreset(): ?string + { + return $this->getOption(self::OPTION_DEMO_PRESET); + } + + /** + * 데모 수량 제한 조회 + */ + public function getDemoLimits(): array + { + return $this->getOption(self::OPTION_DEMO_LIMITS, [ + 'max_items' => 100, + 'max_orders' => 50, + 'max_productions' => 30, + 'max_users' => 5, + 'max_storage_gb' => 1, + 'max_ai_tokens' => 100000, + ]); + } + + /** + * 데모 → 정식 테넌트로 전환 + */ + public function convertToProduction(): void + { + $this->update([ + 'tenant_type' => self::TYPE_STD, + 'demo_expires_at' => null, + 'demo_source_partner_id' => null, + ]); + + // options에서 데모 관련 키 제거 + $options = $this->options ?? []; + unset( + $options[self::OPTION_DEMO_PRESET], + $options[self::OPTION_DEMO_LIMITS], + $options[self::OPTION_DEMO_READ_ONLY] + ); + $this->update(['options' => $options ?: null]); + } } diff --git a/database/migrations/2026_03_13_100000_add_demo_columns_to_tenants_table.php b/database/migrations/2026_03_13_100000_add_demo_columns_to_tenants_table.php new file mode 100644 index 0000000..f0e74a6 --- /dev/null +++ b/database/migrations/2026_03_13_100000_add_demo_columns_to_tenants_table.php @@ -0,0 +1,46 @@ +datetime('demo_expires_at') + ->nullable() + ->after('trial_ends_at') + ->comment('데모 만료일시 (Tier 3: 생성일+30일)'); + + $table->unsignedBigInteger('demo_source_partner_id') + ->nullable() + ->after('demo_expires_at') + ->comment('데모 생성 영업파트너 ID (sales_partners.id)'); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table) { + $table->dropColumn(['demo_expires_at', 'demo_source_partner_id']); + }); + + // tenant_type ENUM 원복 + DB::statement("ALTER TABLE tenants MODIFY COLUMN tenant_type ENUM('STD', 'TPL', 'HQ') NOT NULL DEFAULT 'STD' COMMENT '테넌트 유형: STD=일반, TPL=템플릿, HQ=본사'"); + } +};