option('manifest'); if ($manifestPath === '' || ! File::isFile($manifestPath)) { $this->error('--manifest 옵션이 필수이며 존재하는 파일이어야 합니다.'); return self::INVALID; } // spawn 자식 진입 시 활성 모듈/플러그인의 PSR-4 매핑을 fresh 등록 — 자세한 배경은 // ExecuteUpgradeStepsCommand::handle 의 동일 호출 주석 참조. // (Artisan::call 대신 직접 메서드 호출 — nested Artisan::call 이 outer 명령의 // output buffer 를 덮어쓰는 Laravel 동작 회피) try { app(ExtensionManager::class)->updateComposerAutoload(); } catch (\Throwable $e) { Log::warning('bundled update spawn 자식: updateComposerAutoload 호출 실패', [ 'error' => $e->getMessage(), ]); } $manifest = json_decode(File::get($manifestPath), true); if (! is_array($manifest)) { $this->error('매니페스트 JSON 파싱 실패: '.$manifestPath); return self::FAILURE; } // 부모(core:update)가 운영자 선택을 매니페스트에 실어 보낸다 — 자식이 임의로 정하지 않는다 $rebuildSearchIndex = (bool) $this->option('rebuild-search-index') || (bool) ($manifest['rebuild_search_index'] ?? false); $modules = $manifest['modules'] ?? []; $plugins = $manifest['plugins'] ?? []; $templates = $manifest['templates'] ?? []; $langPacks = $manifest['lang_packs'] ?? []; $moduleManager = app(ModuleManager::class); $pluginManager = app(PluginManager::class); $templateManager = app(TemplateManager::class); $langPackService = app(LanguagePackService::class); $success = 0; $failed = 0; $this->info('── 번들 일괄 업데이트 실행 (spawn child) ──'); foreach ($modules as $entry) { $id = (string) ($entry['identifier'] ?? ''); $strategy = (string) ($entry['strategy'] ?? 'overwrite'); if ($id === '') { continue; } $this->line("→ [모듈] {$id} ({$strategy})"); try { $moduleManager->updateModule($id, true, null, VendorMode::Auto, $strategy, null, 'bundled'); $success++; } catch (\Throwable $e) { $failed++; $this->warn(" 실패: {$e->getMessage()}"); Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'module', 'id' => $id, 'error' => $e->getMessage()]); } } foreach ($plugins as $entry) { $id = (string) ($entry['identifier'] ?? ''); $strategy = (string) ($entry['strategy'] ?? 'overwrite'); if ($id === '') { continue; } $this->line("→ [플러그인] {$id} ({$strategy})"); try { $pluginManager->updatePlugin($id, true, null, VendorMode::Auto, $strategy, null, 'bundled'); $success++; } catch (\Throwable $e) { $failed++; $this->warn(" 실패: {$e->getMessage()}"); Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'plugin', 'id' => $id, 'error' => $e->getMessage()]); } } foreach ($templates as $entry) { $id = (string) ($entry['identifier'] ?? ''); $strategy = (string) ($entry['strategy'] ?? 'overwrite'); if ($id === '') { continue; } $this->line("→ [템플릿] {$id} ({$strategy})"); try { $templateManager->updateTemplate($id, true, null, $strategy, 'bundled'); $success++; } catch (\Throwable $e) { $failed++; $this->warn(" 실패: {$e->getMessage()}"); Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'template', 'id' => $id, 'error' => $e->getMessage()]); } } foreach ($langPacks as $entry) { $id = (string) ($entry['identifier'] ?? ''); if ($id === '') { continue; } $this->line("→ [언어팩] {$id}"); try { $pack = $langPackService->findByIdentifier($id); if (! $pack) { throw new \RuntimeException(__('language_packs.errors.identifier_not_found', ['identifier' => $id])); } $langPackService->performUpdate($pack, true); $success++; } catch (\Throwable $e) { $failed++; $this->warn(" 실패: {$e->getMessage()}"); Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'lang_pack', 'id' => $id, 'error' => $e->getMessage()]); } } $this->newLine(); $this->info("업데이트 완료: 성공 {$success}, 실패 {$failed}"); // 검색 인덱스 재생성 — 운영자가 부모 단계에서 선택했을 때만 수행 (인덱스 잠금·재색인 비용) if ($rebuildSearchIndex) { $report = app(SearchIndexMaintenanceManager::class)->repairStale(); $this->newLine(); $this->info('검색 인덱스: '.$report->summary()); foreach ($report->failed as $identifier => $message) { $this->warn(' '.__('search.index.rebuild_failed_item', ['index' => $identifier, 'error' => $message])); } } // 부모 프로세스가 결과를 복원할 수 있도록 표식 라인으로 페이로드 출력 $this->line(self::RESULT_PREFIX.json_encode([ 'success' => $success, 'failed' => $failed, ], JSON_UNESCAPED_UNICODE)); return $failed > 0 ? self::FAILURE : self::SUCCESS; } }