feat: [demo] 데모 테넌트 관리 API 및 만료 알림 (Phase 3)
- DemoTenantController: 목록/상세/생성/리셋/연장/전환/통계 API - DemoTenantStoreRequest: 고객 체험 테넌트 생성 검증 - DemoTenantService: API용 메서드 추가 (index/show/reset/extend/convert/stats) - CheckDemoExpiredCommand: 만료 임박(7일) 알림 + 만료 테넌트 비활성 처리 - 라우트 등록 (api/v1/demo-tenants, 7개 엔드포인트) - 스케줄러 등록 (04:20 demo:check-expired) - i18n 메시지 추가 (message.demo_tenant.*, error.demo_tenant.*)
This commit is contained in:
92
app/Console/Commands/CheckDemoExpiredCommand.php
Normal file
92
app/Console/Commands/CheckDemoExpiredCommand.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Tenants\Tenant;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 데모 테넌트 만료 체크 및 알림 커맨드
|
||||
*
|
||||
* - 만료 임박 (7일 이내): 파트너에게 알림 로그
|
||||
* - 만료된 테넌트: 비활성 상태로 전환
|
||||
*
|
||||
* 기존 코드 영향 없음: DEMO_TRIAL 테넌트만 대상
|
||||
*
|
||||
* @see docs/features/sales/demo-tenant-policy.md
|
||||
*/
|
||||
class CheckDemoExpiredCommand extends Command
|
||||
{
|
||||
protected $signature = 'demo:check-expired
|
||||
{--dry-run : 실제 변경 없이 대상만 표시}';
|
||||
|
||||
protected $description = '데모 체험 테넌트 만료 체크 및 비활성 처리';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
// 1. 만료 임박 테넌트 (7일 이내)
|
||||
$expiringSoon = Tenant::withoutGlobalScopes()
|
||||
->where('tenant_type', Tenant::TYPE_DEMO_TRIAL)
|
||||
->where('tenant_st_code', '!=', 'expired')
|
||||
->whereNotNull('demo_expires_at')
|
||||
->where('demo_expires_at', '>', now())
|
||||
->where('demo_expires_at', '<=', now()->addDays(7))
|
||||
->get();
|
||||
|
||||
if ($expiringSoon->isNotEmpty()) {
|
||||
$this->info("만료 임박 테넌트: {$expiringSoon->count()}건");
|
||||
foreach ($expiringSoon as $tenant) {
|
||||
$daysLeft = (int) now()->diffInDays($tenant->demo_expires_at, false);
|
||||
$this->line(" - [{$tenant->id}] {$tenant->company_name} (D-{$daysLeft})");
|
||||
|
||||
Log::info('데모 체험 만료 임박', [
|
||||
'tenant_id' => $tenant->id,
|
||||
'company_name' => $tenant->company_name,
|
||||
'expires_at' => $tenant->demo_expires_at->toDateString(),
|
||||
'days_left' => $daysLeft,
|
||||
'partner_id' => $tenant->demo_source_partner_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 이미 만료된 테넌트 → 상태 변경
|
||||
$expired = Tenant::withoutGlobalScopes()
|
||||
->where('tenant_type', Tenant::TYPE_DEMO_TRIAL)
|
||||
->where('tenant_st_code', '!=', 'expired')
|
||||
->whereNotNull('demo_expires_at')
|
||||
->where('demo_expires_at', '<', now())
|
||||
->get();
|
||||
|
||||
if ($expired->isEmpty()) {
|
||||
$this->info('만료 처리 대상 없음');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("만료 처리 대상: {$expired->count()}건");
|
||||
|
||||
foreach ($expired as $tenant) {
|
||||
$this->line(" - [{$tenant->id}] {$tenant->company_name} (만료: {$tenant->demo_expires_at->toDateString()})");
|
||||
|
||||
if (! $this->option('dry-run')) {
|
||||
$tenant->forceFill(['tenant_st_code' => 'expired']);
|
||||
$tenant->save();
|
||||
|
||||
Log::info('데모 체험 만료 처리', [
|
||||
'tenant_id' => $tenant->id,
|
||||
'company_name' => $tenant->company_name,
|
||||
'partner_id' => $tenant->demo_source_partner_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
$this->warn('(dry-run 모드 — 실제 변경 없음)');
|
||||
} else {
|
||||
$this->info(" {$expired->count()}건 만료 처리 완료");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
95
app/Http/Controllers/Api/V1/DemoTenantController.php
Normal file
95
app/Http/Controllers/Api/V1/DemoTenantController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Demo\DemoTenantStoreRequest;
|
||||
use App\Services\Demo\DemoTenantService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 데모 테넌트 관리 API 컨트롤러
|
||||
*
|
||||
* 파트너가 고객 체험 테넌트를 생성/관리하는 엔드포인트
|
||||
*
|
||||
* 기존 코드 영향 없음: 데모 전용 라우트에서만 사용
|
||||
*
|
||||
* @see docs/features/sales/demo-tenant-policy.md
|
||||
*/
|
||||
class DemoTenantController extends Controller
|
||||
{
|
||||
public function __construct(private DemoTenantService $service) {}
|
||||
|
||||
/**
|
||||
* 내가 생성한 데모 테넌트 목록
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.demo_tenant.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 데모 테넌트 상세 조회
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.demo_tenant.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객 체험 테넌트 생성 (Tier 3)
|
||||
*/
|
||||
public function store(DemoTenantStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->createTrialFromApi($request->validated());
|
||||
}, __('message.demo_tenant.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 데모 데이터 리셋
|
||||
*/
|
||||
public function reset(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->resetFromApi($id);
|
||||
}, __('message.demo_tenant.reset'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 체험 기간 연장
|
||||
*/
|
||||
public function extend(int $id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
$days = (int) $request->input('days', 30);
|
||||
|
||||
return $this->service->extendFromApi($id, $days);
|
||||
}, __('message.demo_tenant.extended'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 데모 → 정식 전환
|
||||
*/
|
||||
public function convert(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->convertFromApi($id);
|
||||
}, __('message.demo_tenant.converted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 데모 현황 통계
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->stats();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
35
app/Http/Requests/Demo/DemoTenantStoreRequest.php
Normal file
35
app/Http/Requests/Demo/DemoTenantStoreRequest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Demo;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DemoTenantStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'company_name' => 'required|string|max:100',
|
||||
'email' => 'required|email|max:255',
|
||||
'duration_days' => 'sometimes|integer|min:7|max:60',
|
||||
'preset' => 'sometimes|string|in:manufacturing',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'company_name.required' => '회사명은 필수입니다.',
|
||||
'email.required' => '이메일은 필수입니다.',
|
||||
'email.email' => '올바른 이메일 형식이 아닙니다.',
|
||||
'duration_days.min' => '체험 기간은 최소 7일입니다.',
|
||||
'duration_days.max' => '체험 기간은 최대 60일입니다.',
|
||||
'preset.in' => '유효하지 않은 프리셋입니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* 데모 테넌트 생성/관리 서비스
|
||||
@@ -164,4 +166,304 @@ public function getExpiredTrials(): \Illuminate\Database\Eloquent\Collection
|
||||
->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,4 +527,15 @@
|
||||
'in_use' => '전표에서 사용 중인 계정과목은 삭제할 수 없습니다.',
|
||||
],
|
||||
'tenant_access_denied' => '해당 테넌트에 대한 접근 권한이 없습니다.',
|
||||
|
||||
// 데모 테넌트 관련
|
||||
'demo_tenant' => [
|
||||
'not_found' => '데모 테넌트를 찾을 수 없습니다.',
|
||||
'not_demo' => '데모 테넌트가 아닙니다.',
|
||||
'not_trial' => '고객 체험 테넌트가 아닙니다.',
|
||||
'already_extended' => '이미 연장된 체험 테넌트입니다.',
|
||||
'expired' => '체험 기간이 만료되었습니다.',
|
||||
'invalid_preset' => '유효하지 않은 프리셋입니다.',
|
||||
'not_owned' => '해당 데모 테넌트에 대한 권한이 없습니다.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -591,6 +591,15 @@
|
||||
'photo_uploaded' => '사진이 업로드되었습니다.',
|
||||
],
|
||||
|
||||
// 데모 테넌트 관리
|
||||
'demo_tenant' => [
|
||||
'fetched' => '데모 테넌트를 조회했습니다.',
|
||||
'created' => '데모 테넌트가 생성되었습니다.',
|
||||
'reset' => '데모 데이터가 초기화되었습니다.',
|
||||
'extended' => '체험 기간이 연장되었습니다.',
|
||||
'converted' => '정식 테넌트로 전환되었습니다.',
|
||||
],
|
||||
|
||||
// 일반전표입력
|
||||
'journal_entry' => [
|
||||
'fetched' => '전표 조회 성공',
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
use App\Http\Controllers\Api\V1\ClientController;
|
||||
use App\Http\Controllers\Api\V1\ClientGroupController;
|
||||
use App\Http\Controllers\Api\V1\CompanyController;
|
||||
use App\Http\Controllers\Api\V1\DemoTenantController;
|
||||
use App\Http\Controllers\Api\V1\EstimateController;
|
||||
use App\Http\Controllers\Api\V1\OrderController;
|
||||
use App\Http\Controllers\Api\V1\PricingController;
|
||||
@@ -193,6 +194,17 @@
|
||||
Route::post('/bulk-issue-statement', [SaleController::class, 'bulkIssueStatement'])->name('v1.sales.bulk-issue-statement');
|
||||
});
|
||||
|
||||
// Demo Tenant API (데모 테넌트 관리)
|
||||
Route::prefix('demo-tenants')->group(function () {
|
||||
Route::get('', [DemoTenantController::class, 'index'])->name('v1.demo-tenants.index');
|
||||
Route::get('/stats', [DemoTenantController::class, 'stats'])->name('v1.demo-tenants.stats');
|
||||
Route::post('', [DemoTenantController::class, 'store'])->name('v1.demo-tenants.store');
|
||||
Route::get('/{id}', [DemoTenantController::class, 'show'])->whereNumber('id')->name('v1.demo-tenants.show');
|
||||
Route::post('/{id}/reset', [DemoTenantController::class, 'reset'])->whereNumber('id')->name('v1.demo-tenants.reset');
|
||||
Route::post('/{id}/extend', [DemoTenantController::class, 'extend'])->whereNumber('id')->name('v1.demo-tenants.extend');
|
||||
Route::post('/{id}/convert', [DemoTenantController::class, 'convert'])->whereNumber('id')->name('v1.demo-tenants.convert');
|
||||
});
|
||||
|
||||
// Company API (회사 추가 관리)
|
||||
Route::prefix('companies')->group(function () {
|
||||
Route::post('/check', [CompanyController::class, 'check'])->name('v1.companies.check'); // 사업자등록번호 검증
|
||||
|
||||
@@ -159,6 +159,17 @@
|
||||
|
||||
// ─── 데모 쇼케이스 리셋 ───
|
||||
|
||||
// 매일 새벽 04:20에 데모 체험 테넌트 만료 체크 및 비활성 처리
|
||||
Schedule::command('demo:check-expired')
|
||||
->dailyAt('04:20')
|
||||
->appendOutputTo(storage_path('logs/scheduler.log'))
|
||||
->onSuccess(function () {
|
||||
\Illuminate\Support\Facades\Log::info('✅ demo:check-expired 스케줄러 실행 성공', ['time' => now()]);
|
||||
})
|
||||
->onFailure(function () {
|
||||
\Illuminate\Support\Facades\Log::error('❌ demo:check-expired 스케줄러 실행 실패', ['time' => now()]);
|
||||
});
|
||||
|
||||
// 매일 자정 00:00에 쇼케이스 테넌트 데이터 리셋 + 샘플 재시드
|
||||
Schedule::command('demo:reset-showcase --seed')
|
||||
->dailyAt('00:00')
|
||||
|
||||
Reference in New Issue
Block a user