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:
135
app/Services/Stats/SystemStatService.php
Normal file
135
app/Services/Stats/SystemStatService.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Stats;
|
||||
|
||||
use App\Models\Stats\Daily\StatSystemDaily;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SystemStatService implements StatDomainServiceInterface
|
||||
{
|
||||
public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
{
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
// API 사용량
|
||||
$apiStats = DB::connection('mysql')
|
||||
->table('api_request_logs')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->selectRaw('
|
||||
COUNT(*) as request_count,
|
||||
SUM(CASE WHEN response_status >= 400 THEN 1 ELSE 0 END) as error_count,
|
||||
COALESCE(AVG(duration_ms), 0) as avg_response_ms
|
||||
')
|
||||
->first();
|
||||
|
||||
// 사용자 활동 (고유 사용자 수, 로그인 수)
|
||||
$activeUsers = DB::connection('mysql')
|
||||
->table('api_request_logs')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->whereNotNull('user_id')
|
||||
->distinct('user_id')
|
||||
->count('user_id');
|
||||
|
||||
// personal_access_tokens에 tenant_id 없으므로 user_tenants 조인으로 로그인 수 집계
|
||||
$loginCount = DB::connection('mysql')
|
||||
->table('personal_access_tokens as pat')
|
||||
->join('user_tenants as ut', function ($join) use ($tenantId) {
|
||||
$join->on('pat.tokenable_id', '=', 'ut.user_id')
|
||||
->where('pat.tokenable_type', '=', 'App\\Models\\Members\\User')
|
||||
->where('ut.tenant_id', '=', $tenantId);
|
||||
})
|
||||
->whereDate('pat.created_at', $dateStr)
|
||||
->count();
|
||||
|
||||
// 감사 로그
|
||||
$auditStats = DB::connection('mysql')
|
||||
->table('audit_logs')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->selectRaw("
|
||||
SUM(CASE WHEN action = 'created' THEN 1 ELSE 0 END) as create_count,
|
||||
SUM(CASE WHEN action = 'updated' THEN 1 ELSE 0 END) as update_count,
|
||||
SUM(CASE WHEN action = 'deleted' THEN 1 ELSE 0 END) as delete_count
|
||||
")
|
||||
->first();
|
||||
|
||||
// FCM 알림
|
||||
$fcmStats = DB::connection('mysql')
|
||||
->table('fcm_send_logs')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->selectRaw('
|
||||
COALESCE(SUM(success_count), 0) as sent_count,
|
||||
COALESCE(SUM(failure_count), 0) as failed_count
|
||||
')
|
||||
->first();
|
||||
|
||||
// 파일 업로드
|
||||
$fileStats = DB::connection('mysql')
|
||||
->table('files')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->selectRaw('
|
||||
COUNT(*) as upload_count,
|
||||
COALESCE(SUM(file_size), 0) / 1048576 as upload_size_mb
|
||||
')
|
||||
->first();
|
||||
|
||||
// 결재
|
||||
$approvalSubmitted = DB::connection('mysql')
|
||||
->table('approvals')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('drafted_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$approvalCompleted = DB::connection('mysql')
|
||||
->table('approvals')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('completed_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$approvalAvgHours = DB::connection('mysql')
|
||||
->table('approvals')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('completed_at', $dateStr)
|
||||
->whereNotNull('drafted_at')
|
||||
->whereNotNull('completed_at')
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('AVG(TIMESTAMPDIFF(HOUR, drafted_at, completed_at)) as avg_hours')
|
||||
->value('avg_hours');
|
||||
|
||||
StatSystemDaily::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
||||
[
|
||||
'api_request_count' => $apiStats->request_count ?? 0,
|
||||
'api_error_count' => $apiStats->error_count ?? 0,
|
||||
'api_avg_response_ms' => (int) ($apiStats->avg_response_ms ?? 0),
|
||||
'active_user_count' => $activeUsers,
|
||||
'login_count' => $loginCount,
|
||||
'audit_create_count' => $auditStats->create_count ?? 0,
|
||||
'audit_update_count' => $auditStats->update_count ?? 0,
|
||||
'audit_delete_count' => $auditStats->delete_count ?? 0,
|
||||
'fcm_sent_count' => $fcmStats->sent_count ?? 0,
|
||||
'fcm_failed_count' => $fcmStats->failed_count ?? 0,
|
||||
'file_upload_count' => $fileStats->upload_count ?? 0,
|
||||
'file_upload_size_mb' => $fileStats->upload_size_mb ?? 0,
|
||||
'approval_submitted_count' => $approvalSubmitted,
|
||||
'approval_completed_count' => $approvalCompleted,
|
||||
'approval_avg_hours' => (float) ($approvalAvgHours ?? 0),
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
||||
{
|
||||
// 시스템 도메인은 일간 테이블만 운영
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user