Files
sam-manage/app/Http/Controllers/Sales/DemoTenantController.php

324 lines
11 KiB
PHP
Raw Normal View History

<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesPartner;
use App\Models\Tenants\Tenant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
/**
* 데모 체험 관리 컨트롤러
*
* 영업파트너가 고객에게 SAM을 체험시키기 위한 데모 테넌트 관리
* - 고객 체험 테넌트 생성 (Tier 3)
* - 목록/상세 조회
* - 기간 연장, 정식 전환
*
* @see docs/features/sales/demo-tenant-policy.md
*/
class DemoTenantController extends Controller
{
private const DEMO_TYPES = ['DEMO_SHOWCASE', 'DEMO_PARTNER', 'DEMO_TRIAL'];
private const DEFAULT_LIMITS = [
'max_items' => 100,
'max_orders' => 50,
'max_productions' => 30,
'max_users' => 5,
'max_storage_gb' => 1,
'max_ai_tokens' => 100000,
];
/**
* 데모 체험 관리 메인 화면
*/
public function index(Request $request): View
{
if ($request->header('HX-Request') && ! $request->header('HX-Boosted')) {
return response('', 200)->header('HX-Redirect', route('sales.demo-tenants.index'));
}
$partner = SalesPartner::where('user_id', auth()->id())->first();
$isAdmin = auth()->user()->isHqAdmin ?? false;
$query = Tenant::withoutGlobalScopes()
->whereIn('tenant_type', self::DEMO_TYPES);
if (! $isAdmin && $partner) {
$query->where('demo_source_partner_id', $partner->id);
}
$demos = $query->orderByDesc('created_at')->get()->map(function ($t) {
$t->demo_status = $this->getDemoStatus($t);
$t->remaining_days = $this->getRemainingDays($t->demo_expires_at);
$t->parsed_options = $t->options ?? [];
return $t;
});
$stats = [
'total' => $demos->count(),
'active' => $demos->where('demo_status', 'active')->count(),
'expired' => $demos->where('demo_status', 'expired')->count(),
'converted' => $demos->where('demo_status', 'converted')->count(),
'expiring_soon' => $demos->filter(fn ($d) => $d->demo_status === 'active' && $d->remaining_days !== null && $d->remaining_days <= 7)->count(),
];
return view('sales.demo-tenants.index', compact('demos', 'stats', 'partner', 'isAdmin'));
}
/**
* 목록 부분 새로고침 (HTMX)
*/
public function refreshList(Request $request): View
{
$partner = SalesPartner::where('user_id', auth()->id())->first();
$isAdmin = auth()->user()->isHqAdmin ?? false;
$query = Tenant::withoutGlobalScopes()
->whereIn('tenant_type', self::DEMO_TYPES);
if (! $isAdmin && $partner) {
$query->where('demo_source_partner_id', $partner->id);
}
$demos = $query->orderByDesc('created_at')->get()->map(function ($t) {
$t->demo_status = $this->getDemoStatus($t);
$t->remaining_days = $this->getRemainingDays($t->demo_expires_at);
$t->parsed_options = $t->options ?? [];
return $t;
});
$stats = [
'total' => $demos->count(),
'active' => $demos->where('demo_status', 'active')->count(),
'expired' => $demos->where('demo_status', 'expired')->count(),
'converted' => $demos->where('demo_status', 'converted')->count(),
'expiring_soon' => $demos->filter(fn ($d) => $d->demo_status === 'active' && $d->remaining_days !== null && $d->remaining_days <= 7)->count(),
];
return view('sales.demo-tenants.partials.demo-list', compact('demos', 'stats'));
}
/**
* 상세 조회 모달 (HTMX)
*/
public function show(int $id): View
{
$tenant = $this->findAndAuthorize($id);
$tenant->demo_status = $this->getDemoStatus($tenant);
$tenant->remaining_days = $this->getRemainingDays($tenant->demo_expires_at);
$tenant->parsed_options = $tenant->options ?? [];
// 데이터 건수 조회
$dataCounts = [];
foreach (['clients' => '거래처', 'items' => '품목', 'orders' => '수주', 'quotes' => '견적'] as $table => $label) {
try {
$dataCounts[$table] = ['label' => $label, 'count' => DB::table($table)->where('tenant_id', $id)->count()];
} catch (\Exception $e) {
$dataCounts[$table] = ['label' => $label, 'count' => 0];
}
}
return view('sales.demo-tenants.partials.show-modal', compact('tenant', 'dataCounts'));
}
/**
* 생성 모달 (HTMX)
*/
public function createForm(): View
{
return view('sales.demo-tenants.partials.create-modal');
}
/**
* 고객 체험 테넌트 생성
*/
public function store(Request $request)
{
$request->validate([
'company_name' => 'required|string|max:200',
'email' => 'nullable|email|max:100',
'duration_days' => 'required|integer|min:7|max:60',
'preset' => 'nullable|string|in:manufacturing,none',
]);
$partner = SalesPartner::where('user_id', auth()->id())->first();
$preset = $request->input('preset', 'manufacturing');
$options = [
'demo_preset' => $preset === 'none' ? null : $preset,
'demo_limits' => self::DEFAULT_LIMITS,
'demo_email' => $request->input('email'),
'created_by_user_id' => auth()->id(),
'created_by_user_name' => auth()->user()->name ?? '',
];
$tenant = new Tenant;
$tenant->forceFill([
'company_name' => $request->input('company_name'),
'tenant_type' => 'DEMO_TRIAL',
'tenant_st_code' => 'trial',
'demo_expires_at' => now()->addDays($request->input('duration_days', 30)),
'demo_source_partner_id' => $partner?->id,
'options' => $options,
'is_active' => true,
'storage_limit' => 1073741824,
'storage_used' => 0,
]);
$tenant->save();
if ($request->header('HX-Request')) {
return response('', 200)
->header('HX-Trigger', json_encode(['showToast' => '데모 체험 테넌트가 생성되었습니다.', 'refreshDemoList' => true]));
}
return redirect()->route('sales.demo-tenants.index')->with('success', '데모 체험 테넌트가 생성되었습니다.');
}
/**
* 체험 기간 연장
*/
public function extend(Request $request, int $id)
{
$tenant = $this->findAndAuthorize($id);
if ($tenant->tenant_type !== 'DEMO_TRIAL') {
return $this->htmxError('고객 체험 유형만 연장 가능합니다.');
}
$opts = $tenant->options ?? [];
if (! empty($opts['demo_extended'])) {
return $this->htmxError('이미 연장된 체험입니다. 연장은 1회만 가능합니다.');
}
$days = $request->input('days', 14);
$opts['demo_extended'] = true;
$opts['demo_extended_at'] = now()->toDateTimeString();
$opts['demo_extended_days'] = $days;
$tenant->forceFill([
'demo_expires_at' => $tenant->demo_expires_at
? $tenant->demo_expires_at->addDays($days)
: now()->addDays($days),
'options' => $opts,
'tenant_st_code' => 'trial',
]);
$tenant->save();
return response('', 200)
->header('HX-Trigger', json_encode(['showToast' => "{$days}일 연장되었습니다.", 'refreshDemoList' => true, 'closeModal' => true]));
}
/**
* 정식 전환
*/
public function convert(int $id)
{
$tenant = $this->findAndAuthorize($id);
if (! in_array($tenant->tenant_type, self::DEMO_TYPES)) {
return $this->htmxError('데모 테넌트가 아닙니다.');
}
$opts = $tenant->options ?? [];
unset($opts['demo_preset'], $opts['demo_limits'], $opts['demo_read_only']);
$opts['converted_at'] = now()->toDateTimeString();
$opts['converted_by_user_id'] = auth()->id();
$tenant->forceFill([
'tenant_type' => 'STD',
'tenant_st_code' => 'active',
'demo_expires_at' => null,
'options' => $opts,
]);
$tenant->save();
return response('', 200)
->header('HX-Trigger', json_encode(['showToast' => '정식 테넌트로 전환되었습니다.', 'refreshDemoList' => true, 'closeModal' => true]));
}
/**
* 데모 테넌트 삭제
*/
public function destroy(int $id)
{
$tenant = $this->findAndAuthorize($id);
if (! in_array($tenant->tenant_type, self::DEMO_TYPES)) {
return $this->htmxError('데모 테넌트만 삭제 가능합니다.');
}
// 데이터 존재 확인
foreach (['orders', 'clients', 'items'] as $table) {
try {
if (DB::table($table)->where('tenant_id', $id)->exists()) {
return $this->htmxError('데이터가 존재하는 테넌트는 삭제할 수 없습니다.');
}
} catch (\Exception $e) {
}
}
$tenant->forceDelete();
return response('', 200)
->header('HX-Trigger', json_encode(['showToast' => '데모 테넌트가 삭제되었습니다.', 'refreshDemoList' => true, 'closeModal' => true]));
}
// ── helpers ──
private function findAndAuthorize(int $id): Tenant
{
$tenant = Tenant::withoutGlobalScopes()
->whereIn('tenant_type', self::DEMO_TYPES)
->findOrFail($id);
$isAdmin = auth()->user()->isHqAdmin ?? false;
if (! $isAdmin) {
$partner = SalesPartner::where('user_id', auth()->id())->first();
if (! $partner || $tenant->demo_source_partner_id !== $partner->id) {
abort(403, '권한이 없습니다.');
}
}
return $tenant;
}
private function getDemoStatus(Tenant $tenant): string
{
if ($tenant->tenant_type === 'DEMO_SHOWCASE') {
return 'showcase';
}
if ($tenant->tenant_st_code === 'converted' || $tenant->tenant_type === 'STD') {
return 'converted';
}
if ($tenant->tenant_st_code === 'expired') {
return 'expired';
}
if ($tenant->demo_expires_at && $tenant->demo_expires_at->isPast()) {
return 'expired';
}
return 'active';
}
private function getRemainingDays($expiresAt): ?int
{
if (! $expiresAt) {
return null;
}
return max(0, (int) now()->diffInDays($expiresAt, false));
}
private function htmxError(string $message)
{
return response('', 200)
->header('HX-Trigger', json_encode(['showToast' => $message, 'toastType' => 'error']));
}
}