[...], 'modules' => [...], 'templates' => [...]] */ public function autoDeactivated(AutoDeactivatedListRequest $request): JsonResponse { $repos = [ 'plugins' => app(PluginRepositoryInterface::class), 'modules' => app(ModuleRepositoryInterface::class), 'templates' => app(TemplateRepositoryInterface::class), ]; // 사용자별 dismiss 이력 (Listener 와 동일 SSoT — Service 가 단일 진입점) $dismissedIds = $this->alertService->getDismissedAlertIds($request->user()?->id); $result = []; foreach ($repos as $type => $repo) { $singular = rtrim($type, 's'); $result[$type] = $repo->findAutoDeactivated() ->reject(fn ($record) => $this->isHiddenExtension($singular, $record->identifier)) ->reject(fn ($record) => in_array("compat_{$type}_{$record->identifier}", $dismissedIds, true)) ->map(function ($record) { return [ 'identifier' => $record->identifier, 'incompatible_required_version' => $record->incompatible_required_version, 'deactivated_at' => $record->deactivated_at, ]; })->values(); } return ResponseHelper::success('extensions.alerts.auto_deactivated_listed', [ 'items' => $result, 'current_core_version' => CoreVersionChecker::getCoreVersion(), ]); } /** * 비호환으로 자동 비활성화된 확장의 원클릭 복구 (재활성화). * * @param RecoverRequest $request 요청 * @param string $type 확장 타입 (module|plugin|template) * @param string $identifier 확장 식별자 * @return JsonResponse 복구 결과 */ public function recover(RecoverRequest $request, string $type, string $identifier): JsonResponse { $repo = $this->resolveRepository($type); if (! $repo) { return ResponseHelper::error('extensions.errors.invalid_type', 422); } $record = $repo->findByIdentifier($identifier); if (! $record) { return ResponseHelper::error('extensions.errors.not_found', 404); } // hidden 확장은 사용자 노출 대상 아님 — 직접 endpoint 호출로 우회 차단 if ($this->isHiddenExtension($type, $identifier)) { return ResponseHelper::error('extensions.errors.hidden_extension', 422, [ 'error_code' => 'hidden_extension', ]); } $reasonValue = $record->deactivated_reason instanceof \BackedEnum ? $record->deactivated_reason->value : $record->deactivated_reason; if ($reasonValue !== DeactivationReason::IncompatibleCore->value) { return ResponseHelper::error('extensions.errors.not_auto_deactivated', 422, [ 'error_code' => 'not_auto_deactivated', ]); } // 재호환 재검증 (글로벌 핸들러가 비호환 시 422 + core_version_mismatch 자동 변환) CoreVersionChecker::validateExtension( $record->incompatible_required_version, $identifier, $type, ); // 활성화 호출 (force=false — 검증 이미 통과했으므로 재검증 의도) $manager = $this->resolveManager($type); match ($type) { 'plugin' => $manager->activatePlugin($identifier), 'module' => $manager->activateModule($identifier), 'template' => $manager->activateTemplate($identifier), }; return ResponseHelper::success('extensions.alerts.recovered_success', [ 'extension_type' => $type, 'identifier' => $identifier, ]); } /** * 호환성 알림 dismiss (사용자별). * * @param DismissAlertRequest $request 요청 * @param string $type 확장 타입 (module|plugin|template) * @param string $identifier 확장 식별자 * @return JsonResponse dismiss 결과 */ public function dismiss(DismissAlertRequest $request, string $type, string $identifier): JsonResponse { $userId = $request->user()?->id; $alertId = "compat_{$type}s_{$identifier}"; $this->alertService->dismissAlert($alertId, $userId); // 재호환 알림도 함께 dismiss (해당 alert 라면 즉시 사라지나 캐시 만료/감지 갱신 시 재노출) $this->alertService->dismissAlert("recover_{$type}s_{$identifier}", $userId); return ResponseHelper::success('extensions.alerts.dismissed', [ 'alert_id' => $alertId, ]); } /** * 타입에 해당하는 Repository 를 반환합니다. * * @param string $type 확장 타입 * @return mixed|null Repository 또는 null */ protected function resolveRepository(string $type): mixed { return match ($type) { 'plugin' => app(PluginRepositoryInterface::class), 'module' => app(ModuleRepositoryInterface::class), 'template' => app(TemplateRepositoryInterface::class), default => null, }; } /** * 타입에 해당하는 Manager 를 반환합니다. * * @param string $type 확장 타입 * @return mixed Manager 인스턴스 */ protected function resolveManager(string $type): mixed { return match ($type) { 'plugin' => app(PluginManagerInterface::class), 'module' => app(ModuleManagerInterface::class), 'template' => app(TemplateManagerInterface::class), }; } /** * 확장이 hidden(학습용 샘플 등) manifest 플래그를 가졌는지 판정합니다. * * Listener 의 동일 로직과 정합 — 자동 비활성화/재호환 알림을 사용자에게 노출하지 않는 정책. * * @param string $singularType module|plugin|template * @param string $identifier 확장 식별자 * @return bool */ protected function isHiddenExtension(string $singularType, string $identifier): bool { try { $manager = $this->resolveManager($singularType); if ($manager === null) { return false; } $extension = match ($singularType) { 'plugin' => $manager->getPlugin($identifier), 'module' => $manager->getModule($identifier), 'template' => $manager->getTemplate($identifier), }; if (is_array($extension)) { return ! empty($extension['hidden']); } if (is_object($extension) && method_exists($extension, 'isHidden')) { return (bool) $extension->isHidden(); } } catch (\Throwable $e) { // 판정 실패 시 안전 측 — hidden 아닌 것으로 간주 (기존 노출 정책 유지) } return false; } }