From 45c30aa2aa4c05c8d7e52329bf535883cbfecc47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 13 Mar 2026 21:52:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[tenant]=20=EB=8D=B0=EB=AA=A8=20?= =?UTF-8?q?=ED=85=8C=EB=84=8C=ED=8A=B8=20=EC=A7=80=EC=9B=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tenants.tenant_type ENUM 확장: DEMO_SHOWCASE, DEMO_PARTNER, DEMO_TRIAL - demo_expires_at, demo_source_partner_id 컬럼 추가 - Tenant 모델에 데모 관련 메서드 추가 (isDemoTenant, isDemoShowcase 등) - getOption/setOption 헬퍼 메서드 추가 - 데모→정식 전환 convertToProduction() 메서드 --- app/Models/Tenants/Tenant.php | 158 ++++++++++++++++++ ...0000_add_demo_columns_to_tenants_table.php | 46 +++++ 2 files changed, 204 insertions(+) create mode 100644 database/migrations/2026_03_13_100000_add_demo_columns_to_tenants_table.php 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=본사'"); + } +};