feat: sam_stat P2 도메인 + 통계 API + 대시보드 전환 (Phase 4)

- 4.1: stat_project_monthly + ProjectStatService (건설/프로젝트 월간)
- 4.2: stat_system_daily + SystemStatService (API/감사/FCM/파일/결재)
- 4.3: stat_events, stat_snapshots + StatEventService + StatEventObserver
- 4.4: StatController (summary/daily/monthly/alerts) + StatQueryService + FormRequest 3개 + routes/stats.php
- 4.5: DashboardService sam_stat 우선 조회 + 원본 DB 폴백 패턴

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 21:56:53 +09:00
parent 595e3d59b4
commit 4d8dac1091
22 changed files with 1011 additions and 7 deletions

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
protected $connection = 'sam_stat';
public function up(): void
{
Schema::connection('sam_stat')->create('stat_project_monthly', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->smallInteger('stat_year');
$table->tinyInteger('stat_month');
// 프로젝트 현황
$table->unsignedInteger('active_site_count')->default(0);
$table->unsignedInteger('completed_site_count')->default(0);
$table->unsignedInteger('new_contract_count')->default(0);
$table->decimal('contract_total_amount', 18, 2)->default(0);
// 원가
$table->decimal('expected_expense_total', 18, 2)->default(0);
$table->decimal('actual_expense_total', 18, 2)->default(0);
$table->decimal('labor_cost_total', 18, 2)->default(0);
$table->decimal('material_cost_total', 18, 2)->default(0);
// 수익률
$table->decimal('gross_profit', 18, 2)->default(0);
$table->decimal('gross_profit_rate', 5, 2)->default(0);
// 이슈
$table->unsignedInteger('handover_report_count')->default(0);
$table->unsignedInteger('structure_review_count')->default(0);
$table->timestamps();
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
$table->index(['stat_year', 'stat_month'], 'idx_year_month');
});
}
public function down(): void
{
Schema::connection('sam_stat')->dropIfExists('stat_project_monthly');
}
};

View File

@@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
protected $connection = 'sam_stat';
public function up(): void
{
Schema::connection('sam_stat')->create('stat_system_daily', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->date('stat_date');
// API 사용량
$table->unsignedInteger('api_request_count')->default(0);
$table->unsignedInteger('api_error_count')->default(0);
$table->unsignedInteger('api_avg_response_ms')->default(0);
// 사용자 활동
$table->unsignedInteger('active_user_count')->default(0);
$table->unsignedInteger('login_count')->default(0);
// 감사
$table->unsignedInteger('audit_create_count')->default(0);
$table->unsignedInteger('audit_update_count')->default(0);
$table->unsignedInteger('audit_delete_count')->default(0);
// 알림
$table->unsignedInteger('fcm_sent_count')->default(0);
$table->unsignedInteger('fcm_failed_count')->default(0);
// 파일
$table->unsignedInteger('file_upload_count')->default(0);
$table->decimal('file_upload_size_mb', 10, 2)->default(0);
// 결재
$table->unsignedInteger('approval_submitted_count')->default(0);
$table->unsignedInteger('approval_completed_count')->default(0);
$table->decimal('approval_avg_hours', 8, 2)->default(0);
$table->timestamps();
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
$table->index('stat_date', 'idx_date');
});
}
public function down(): void
{
Schema::connection('sam_stat')->dropIfExists('stat_system_daily');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
protected $connection = 'sam_stat';
public function up(): void
{
Schema::connection('sam_stat')->create('stat_events', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->string('domain', 50);
$table->string('event_type', 100);
$table->string('entity_type', 100);
$table->unsignedBigInteger('entity_id');
$table->json('payload')->nullable();
$table->timestamp('occurred_at');
$table->index(['tenant_id', 'domain'], 'idx_tenant_domain');
$table->index('occurred_at', 'idx_occurred');
$table->index(['entity_type', 'entity_id'], 'idx_entity');
});
}
public function down(): void
{
Schema::connection('sam_stat')->dropIfExists('stat_events');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
protected $connection = 'sam_stat';
public function up(): void
{
Schema::connection('sam_stat')->create('stat_snapshots', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->date('snapshot_date');
$table->string('domain', 50);
$table->string('snapshot_type', 50)->default('daily');
$table->json('data');
$table->timestamp('created_at')->nullable();
$table->unique(['tenant_id', 'snapshot_date', 'domain', 'snapshot_type'], 'uk_tenant_date_domain');
$table->index('snapshot_date', 'idx_date');
});
}
public function down(): void
{
Schema::connection('sam_stat')->dropIfExists('stat_snapshots');
}
};