'integer', 'checkpoint_index' => 'integer', 'is_checked' => 'boolean', 'checked_at' => 'datetime', ]; /** * 테넌트 관계 */ public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } /** * 체크한 사용자 관계 */ public function checkedByUser(): BelongsTo { return $this->belongsTo(User::class, 'checked_by'); } /** * 사용자 관계 (하위 호환성) */ public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } /** * 체크포인트 토글 */ public static function toggle(int $tenantId, string $scenarioType, int $stepId, string $checkpointId, bool $checked, ?int $userId = null): self { $checklist = self::firstOrNew([ 'tenant_id' => $tenantId, 'scenario_type' => $scenarioType, 'step_id' => $stepId, 'checkpoint_id' => $checkpointId, ]); $checklist->is_checked = $checked; $checklist->checked_at = $checked ? now() : null; $checklist->checked_by = $checked ? ($userId ?? auth()->id()) : null; $checklist->save(); return $checklist; } /** * 특정 테넌트/시나리오의 체크리스트 조회 */ public static function getChecklist(int $tenantId, string $scenarioType): array { $items = self::where('tenant_id', $tenantId) ->where('scenario_type', $scenarioType) ->where('is_checked', true) ->get(); $result = []; foreach ($items as $item) { $key = "{$item->step_id}_{$item->checkpoint_id}"; $result[$key] = [ 'checked_at' => $item->checked_at?->toDateTimeString(), 'checked_by' => $item->checked_by, ]; } return $result; } /** * 진행률 계산 */ public static function calculateProgress(int $tenantId, string $scenarioType, array $steps): array { $checklist = self::getChecklist($tenantId, $scenarioType); $totalCheckpoints = 0; $completedCheckpoints = 0; $stepProgress = []; foreach ($steps as $step) { $stepCompleted = 0; $stepTotal = count($step['checkpoints'] ?? []); $totalCheckpoints += $stepTotal; foreach ($step['checkpoints'] as $checkpoint) { $key = "{$step['id']}_{$checkpoint['id']}"; if (isset($checklist[$key])) { $completedCheckpoints++; $stepCompleted++; } } $stepProgress[$step['id']] = [ 'total' => $stepTotal, 'completed' => $stepCompleted, 'percentage' => $stepTotal > 0 ? round(($stepCompleted / $stepTotal) * 100) : 0, ]; } return [ 'total' => $totalCheckpoints, 'completed' => $completedCheckpoints, 'percentage' => $totalCheckpoints > 0 ? round(($completedCheckpoints / $totalCheckpoints) * 100) : 0, 'steps' => $stepProgress, ]; } /** * 시나리오 타입 스코프 */ public function scopeByScenarioType($query, string $type) { return $query->where('scenario_type', $type); } /** * 체크된 항목만 스코프 */ public function scopeChecked($query) { return $query->where('is_checked', true); } }