feat: [tenant] 데모 테넌트 지원 추가 (Phase 1)

- tenants.tenant_type ENUM 확장: DEMO_SHOWCASE, DEMO_PARTNER, DEMO_TRIAL
- demo_expires_at, demo_source_partner_id 컬럼 추가
- Tenant 모델에 데모 관련 메서드 추가 (isDemoTenant, isDemoShowcase 등)
- getOption/setOption 헬퍼 메서드 추가
- 데모→정식 전환 convertToProduction() 메서드
This commit is contained in:
김보곤
2026-03-13 21:52:12 +09:00
parent 85d5b98966
commit 45c30aa2aa
2 changed files with 204 additions and 0 deletions

View File

@@ -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]);
}
}

View File

@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 데모 테넌트 지원을 위한 tenants 테이블 확장
*
* - tenant_type ENUM에 데모 유형 추가 (DEMO_SHOWCASE, DEMO_PARTNER, DEMO_TRIAL)
* - demo_expires_at: 체험 만료일 (Tier 3 고객 체험 30일 제한)
* - demo_source_partner_id: 데모를 생성한 영업파트너 ID
*
* @see docs/features/sales/demo-tenant-policy.md
*/
public function up(): void
{
// tenant_type ENUM 확장: 기존 STD/TPL/HQ + 데모 유형 3개
DB::statement("ALTER TABLE tenants MODIFY COLUMN tenant_type ENUM('STD', 'TPL', 'HQ', 'DEMO_SHOWCASE', 'DEMO_PARTNER', 'DEMO_TRIAL') NOT NULL DEFAULT 'STD' COMMENT '테넌트 유형: STD=일반, TPL=템플릿, HQ=본사, DEMO_SHOWCASE=공용데모, DEMO_PARTNER=파트너데모, DEMO_TRIAL=고객체험'");
Schema::table('tenants', function (Blueprint $table) {
$table->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=본사'");
}
};