100, 'max_orders' => 50, 'max_productions' => 30, 'max_users' => 5, 'max_storage_gb' => 1, 'max_ai_tokens' => 100000, ]; /** * 사용 가능한 프리셋 목록 */ private const AVAILABLE_PRESETS = [ 'manufacturing', // 제조업 기본 // 향후 추가 예정: // 'blinds', // 블라인드/스크린 // 'construction', // 시공/건설 // 'distribution', // 유통/도소매 ]; /** * 파트너 데모 테넌트 생성 (Tier 2) * 파트너 승인 시 호출 */ public function createPartnerDemo(int $partnerId, string $preset = 'manufacturing'): Tenant { $tenant = new Tenant; $tenant->forceFill([ 'company_name' => '파트너데모_'.$partnerId, 'code' => 'DEMO_P_'.$partnerId, 'email' => 'demo-partner-'.$partnerId.'@codebridge-x.com', 'tenant_st_code' => 'active', 'tenant_type' => Tenant::TYPE_DEMO_PARTNER, 'demo_source_partner_id' => $partnerId, 'options' => [ Tenant::OPTION_DEMO_PRESET => $preset, Tenant::OPTION_DEMO_LIMITS => self::DEFAULT_DEMO_LIMITS, ], ]); $tenant->save(); Log::info('파트너 데모 테넌트 생성', [ 'tenant_id' => $tenant->id, 'partner_id' => $partnerId, 'preset' => $preset, ]); return $tenant; } /** * 고객 체험 테넌트 생성 (Tier 3) * 파트너가 요청 → 본사 승인 후 호출 */ public function createTrialDemo( int $partnerId, string $companyName, string $email, int $durationDays = 30, string $preset = 'manufacturing' ): Tenant { $tenant = new Tenant; $tenant->forceFill([ 'company_name' => $companyName, 'code' => 'DEMO_T_'.strtoupper(substr(md5(uniqid()), 0, 8)), 'email' => $email, 'tenant_st_code' => 'trial', 'tenant_type' => Tenant::TYPE_DEMO_TRIAL, 'demo_expires_at' => now()->addDays($durationDays), 'demo_source_partner_id' => $partnerId, 'options' => [ Tenant::OPTION_DEMO_PRESET => $preset, Tenant::OPTION_DEMO_LIMITS => self::DEFAULT_DEMO_LIMITS, ], ]); $tenant->save(); Log::info('고객 체험 테넌트 생성', [ 'tenant_id' => $tenant->id, 'partner_id' => $partnerId, 'company_name' => $companyName, 'expires_at' => $tenant->demo_expires_at->toDateString(), ]); return $tenant; } /** * 체험 기간 연장 (최대 1회, 30일) */ public function extendTrial(Tenant $tenant, int $days = 30): bool { if (! $tenant->isDemoTrial()) { return false; } // 이미 연장한 이력이 있는지 체크 (options에 기록) if ($tenant->getOption('demo_extended', false)) { return false; } $newExpiry = ($tenant->demo_expires_at ?? now())->addDays($days); $tenant->forceFill(['demo_expires_at' => $newExpiry]); $tenant->setOption('demo_extended', true); $tenant->save(); Log::info('고객 체험 기간 연장', [ 'tenant_id' => $tenant->id, 'new_expires_at' => $newExpiry->toDateString(), ]); return true; } /** * 데모 → 정식 전환 */ public function convertToProduction(Tenant $tenant): void { $tenant->convertToProduction(); Log::info('데모 → 정식 테넌트 전환', [ 'tenant_id' => $tenant->id, 'company_name' => $tenant->company_name, ]); } /** * 사용 가능한 프리셋 목록 */ public function getAvailablePresets(): array { return self::AVAILABLE_PRESETS; } /** * 만료된 체험 테넌트 조회 */ public function getExpiredTrials(): \Illuminate\Database\Eloquent\Collection { return Tenant::withoutGlobalScopes() ->where('tenant_type', Tenant::TYPE_DEMO_TRIAL) ->whereNotNull('demo_expires_at') ->where('demo_expires_at', '<', now()) ->get(); } // ────────────────────────────────────────────── // Phase 3: API 엔드포인트용 메서드 // ────────────────────────────────────────────── /** * 내가 생성한 데모 테넌트 목록 */ public function index(array $params): array { $userId = $this->apiUserId(); // 현재 사용자의 파트너 ID 조회 $partnerId = DB::table('sales_partners') ->where('user_id', $userId) ->value('id'); $query = Tenant::withoutGlobalScopes() ->whereIn('tenant_type', Tenant::DEMO_TYPES); // 파트너인 경우 자기가 생성한 데모만 조회 if ($partnerId) { $query->where('demo_source_partner_id', $partnerId); } // 상태 필터 if (! empty($params['status'])) { match ($params['status']) { 'active' => $query->where(function ($q) { $q->whereNull('demo_expires_at') ->orWhere('demo_expires_at', '>', now()); }), 'expired' => $query->whereNotNull('demo_expires_at') ->where('demo_expires_at', '<', now()), default => null, }; } // 타입 필터 if (! empty($params['type'])) { $query->where('tenant_type', $params['type']); } $tenants = $query->orderByDesc('created_at')->get(); return $tenants->map(function (Tenant $t) { return $this->formatTenantResponse($t); })->toArray(); } /** * 데모 테넌트 상세 조회 */ public function show(int $id): array { $tenant = $this->findDemoTenant($id); $response = $this->formatTenantResponse($tenant); // 상세 조회 시 데이터 현황도 포함 $response['data_counts'] = $this->getDataCounts($tenant->id); return $response; } /** * API에서 고객 체험 테넌트 생성 */ public function createTrialFromApi(array $data): array { $userId = $this->apiUserId(); $partnerId = DB::table('sales_partners') ->where('user_id', $userId) ->value('id'); // 파트너가 아닌 경우 관리자 권한으로 생성 (partnerId = 0) $partnerId = $partnerId ?? 0; $preset = $data['preset'] ?? 'manufacturing'; $durationDays = $data['duration_days'] ?? 30; $tenant = $this->createTrialDemo( $partnerId, $data['company_name'], $data['email'], $durationDays, $preset ); // 샘플 데이터 시드 try { $seeder = new \Database\Seeders\Demo\ManufacturingPresetSeeder; $seeder->run($tenant->id); } catch (\Exception $e) { Log::warning('데모 샘플 데이터 시드 실패', [ 'tenant_id' => $tenant->id, 'error' => $e->getMessage(), ]); } return $this->formatTenantResponse($tenant); } /** * API에서 데모 데이터 리셋 */ public function resetFromApi(int $id): array { $tenant = $this->findDemoTenant($id); $this->checkOwnership($tenant); // 리셋 커맨드의 테이블 목록 사용 $tables = [ 'order_item_components', 'order_items', 'order_histories', 'orders', 'quotes', 'production_results', 'production_plans', 'material_inspection_items', 'material_inspections', 'material_receipts', 'lot_sales', 'lots', 'price_histories', 'product_components', 'items', 'clients', 'departments', 'audit_logs', ]; DB::beginTransaction(); try { $totalDeleted = 0; foreach ($tables as $table) { if (! \Schema::hasTable($table) || ! \Schema::hasColumn($table, 'tenant_id')) { continue; } $totalDeleted += DB::table($table)->where('tenant_id', $tenant->id)->delete(); } DB::commit(); } catch (\Exception $e) { DB::rollBack(); throw $e; } // 재시드 $preset = $tenant->getDemoPreset() ?? 'manufacturing'; try { $seeder = new \Database\Seeders\Demo\ManufacturingPresetSeeder; $seeder->run($tenant->id); } catch (\Exception $e) { Log::warning('리셋 후 재시드 실패', [ 'tenant_id' => $tenant->id, 'error' => $e->getMessage(), ]); } Log::info('데모 데이터 API 리셋', [ 'tenant_id' => $tenant->id, 'deleted_count' => $totalDeleted, ]); return [ 'tenant_id' => $tenant->id, 'deleted_count' => $totalDeleted, 'data_counts' => $this->getDataCounts($tenant->id), ]; } /** * API에서 체험 기간 연장 */ public function extendFromApi(int $id, int $days = 30): array { $tenant = $this->findDemoTenant($id); $this->checkOwnership($tenant); if (! $tenant->isDemoTrial()) { return ['error' => __('error.demo_tenant.not_trial'), 'code' => 400]; } if (! $this->extendTrial($tenant, $days)) { return ['error' => __('error.demo_tenant.already_extended'), 'code' => 400]; } return $this->formatTenantResponse($tenant->fresh()); } /** * API에서 데모 → 정식 전환 */ public function convertFromApi(int $id): array { $tenant = $this->findDemoTenant($id); $this->checkOwnership($tenant); $this->convertToProduction($tenant); return $this->formatTenantResponse($tenant->fresh()); } /** * 데모 현황 통계 */ public function stats(): array { $userId = $this->apiUserId(); $partnerId = DB::table('sales_partners') ->where('user_id', $userId) ->value('id'); $baseQuery = Tenant::withoutGlobalScopes() ->whereIn('tenant_type', Tenant::DEMO_TYPES); if ($partnerId) { $baseQuery->where('demo_source_partner_id', $partnerId); } $all = (clone $baseQuery)->get(); $active = $all->filter(fn (Tenant $t) => ! $t->isDemoExpired()); $expired = $all->filter(fn (Tenant $t) => $t->isDemoExpired()); $converted = Tenant::withoutGlobalScopes() ->where('tenant_type', Tenant::TYPE_STD) ->when($partnerId, fn ($q) => $q->where('demo_source_partner_id', $partnerId)) ->whereNotNull('demo_source_partner_id') ->count(); return [ 'total' => $all->count(), 'active' => $active->count(), 'expired' => $expired->count(), 'converted' => $converted, 'by_type' => [ 'showcase' => $all->where('tenant_type', Tenant::TYPE_DEMO_SHOWCASE)->count(), 'partner' => $all->where('tenant_type', Tenant::TYPE_DEMO_PARTNER)->count(), 'trial' => $all->where('tenant_type', Tenant::TYPE_DEMO_TRIAL)->count(), ], ]; } // ────────────────────────────────────────────── // Private Helpers // ────────────────────────────────────────────── private function findDemoTenant(int $id): Tenant { $tenant = Tenant::withoutGlobalScopes()->find($id); if (! $tenant || ! $tenant->isDemoTenant()) { throw new NotFoundHttpException(__('error.demo_tenant.not_found')); } return $tenant; } private function checkOwnership(Tenant $tenant): void { $userId = $this->apiUserId(); $partnerId = DB::table('sales_partners') ->where('user_id', $userId) ->value('id'); // 파트너가 아닌 경우 (관리자) → 통과 if (! $partnerId) { return; } // 파트너인 경우 → 자기가 생성한 데모만 접근 가능 if ($tenant->demo_source_partner_id !== $partnerId) { throw new NotFoundHttpException(__('error.demo_tenant.not_owned')); } } private function formatTenantResponse(Tenant $tenant): array { return [ 'id' => $tenant->id, 'company_name' => $tenant->company_name, 'code' => $tenant->code, 'email' => $tenant->email, 'tenant_type' => $tenant->tenant_type, 'tenant_st_code' => $tenant->tenant_st_code, 'demo_preset' => $tenant->getDemoPreset(), 'demo_limits' => $tenant->getDemoLimits(), 'demo_expires_at' => $tenant->demo_expires_at?->toDateString(), 'demo_source_partner_id' => $tenant->demo_source_partner_id, 'is_expired' => $tenant->isDemoExpired(), 'is_extended' => (bool) $tenant->getOption('demo_extended', false), 'created_at' => $tenant->created_at?->toDateString(), ]; } private function getDataCounts(int $tenantId): array { $tables = ['departments', 'clients', 'items', 'quotes', 'orders']; $counts = []; foreach ($tables as $table) { if (\Schema::hasTable($table) && \Schema::hasColumn($table, 'tenant_id')) { $counts[$table] = DB::table($table)->where('tenant_id', $tenantId)->count(); } } return $counts; } }