Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지 않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를 --no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는 `composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를 찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상 종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다. (sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10) 3계층으로 막는다. 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다. 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다. 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI 자체에 도달할 수 없다). 업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고 CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의 복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다. 실측 중 드러난 결함 2건을 함께 고쳤다. - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다. - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다). 인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인 카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를 정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다. 코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3). 코어 7.0.10 → 7.0.11.
537 lines
21 KiB
PHP
537 lines
21 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands\Core\Concerns;
|
|
|
|
use App\Console\Commands\Core\ExecuteBundledUpdatesCommand;
|
|
use App\Extension\ModuleManager;
|
|
use App\Extension\PluginManager;
|
|
use App\Extension\TemplateManager;
|
|
use App\Extension\Vendor\VendorMode;
|
|
use App\Search\SearchIndexMaintenanceManager;
|
|
use App\Services\CoreUpdateService;
|
|
use App\Services\LanguagePackService;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* 코어 업데이트 완료 후 _bundled 번들에 신버전이 포함된
|
|
* 모듈/플러그인/템플릿을 일괄 업데이트하는 대화형 프롬프트.
|
|
*
|
|
* CoreUpdateCommand 에서 사용되며, 다음 단계로 동작한다.
|
|
* 1) 각 확장 매니저의 checkAllXxxForUpdates() 로 번들 업데이트 감지
|
|
* 2) 감지 목록 표시 + 일괄 업데이트 진행 여부 확인
|
|
* 3) 동의 시 전역 레이아웃 전략(overwrite|keep) 1회 질의
|
|
* 4) 확장별 예외 변경 질의 (choice 로 다중 선택)
|
|
* 5) 각 확장 매니저의 updateXxx() 호출
|
|
* 6) 결과 요약 출력
|
|
*
|
|
* --force 옵션이 지정된 경우: 프롬프트 스킵 + 전역 전략 'overwrite' 로 즉시 실행.
|
|
*
|
|
* 본 트레이트를 사용하는 Command 는 HasUnifiedConfirm 트레이트도 함께 사용해야 한다
|
|
* (yes/no 입력 정규화 및 재질문 루프 제공).
|
|
*/
|
|
trait BundledExtensionUpdatePrompt
|
|
{
|
|
/**
|
|
* 번들 업데이트 목록 수집.
|
|
*
|
|
* @return array{modules: array, plugins: array, templates: array, lang_packs: array}
|
|
*/
|
|
protected function collectBundledUpdates(
|
|
ModuleManager $moduleManager,
|
|
PluginManager $pluginManager,
|
|
TemplateManager $templateManager,
|
|
): array {
|
|
// CoreUpdateService::collectBundledExtensionUpdates() 를 통해 _bundled manifest 버전을
|
|
// DB 현재 버전과 직접 비교한다. Manager::checkXxxUpdate() 의 "GitHub 엄격 우선" 정책을
|
|
// 우회하여 GitHub 미릴리스 상태에서도 _bundled 신버전을 정확히 감지.
|
|
$extUpdates = app(CoreUpdateService::class)->collectBundledExtensionUpdates();
|
|
|
|
// 언어팩도 동일 패턴으로 _bundled vs DB 직접 비교 (LanguagePackService::collectBundledLangPackUpdates).
|
|
$extUpdates['lang_packs'] = app(LanguagePackService::class)->collectBundledLangPackUpdates();
|
|
|
|
return $extUpdates;
|
|
}
|
|
|
|
/**
|
|
* 일괄 업데이트 UX 를 실행하고 결과를 반환한다.
|
|
*
|
|
* @return array{success: int, failed: int, skipped: int, has_updates: bool}
|
|
*/
|
|
protected function runBundledExtensionUpdatePrompt(
|
|
ModuleManager $moduleManager,
|
|
PluginManager $pluginManager,
|
|
TemplateManager $templateManager,
|
|
bool $force,
|
|
): array {
|
|
$updates = $this->collectBundledUpdates($moduleManager, $pluginManager, $templateManager);
|
|
$updates['lang_packs'] = $updates['lang_packs'] ?? [];
|
|
$total = count($updates['modules']) + count($updates['plugins']) + count($updates['templates']) + count($updates['lang_packs']);
|
|
|
|
if ($total === 0) {
|
|
$this->info('활성 확장이 최신 번들과 일치합니다.');
|
|
|
|
return ['success' => 0, 'failed' => 0, 'skipped' => 0, 'has_updates' => false];
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info('번들에 새 버전이 포함된 확장이 감지되었습니다:');
|
|
foreach ($updates['modules'] as $m) {
|
|
$this->line(" [모듈] {$m['identifier']} {$m['current_version']} → {$m['latest_version']}");
|
|
}
|
|
foreach ($updates['plugins'] as $p) {
|
|
$this->line(" [플러그인] {$p['identifier']} {$p['current_version']} → {$p['latest_version']}");
|
|
}
|
|
foreach ($updates['templates'] as $t) {
|
|
$this->line(" [템플릿] {$t['identifier']} {$t['current_version']} → {$t['latest_version']}");
|
|
}
|
|
foreach ($updates['lang_packs'] as $lp) {
|
|
$this->line(" [언어팩] {$lp['identifier']} {$lp['current_version']} → {$lp['latest_version']}");
|
|
}
|
|
$this->newLine();
|
|
|
|
if (! $force && ! $this->unifiedConfirm('일괄 업데이트를 진행하시겠습니까?', true)) {
|
|
$this->info('일괄 업데이트를 건너뜁니다.');
|
|
|
|
return ['success' => 0, 'failed' => 0, 'skipped' => $total, 'has_updates' => true];
|
|
}
|
|
|
|
// 전역 레이아웃 전략
|
|
$globalStrategy = 'overwrite';
|
|
if (! $force) {
|
|
$answer = $this->choice(
|
|
'전역 레이아웃 전략을 선택하세요',
|
|
['overwrite' => '모든 레이아웃을 번들로 덮어쓰기', 'keep' => '사용자가 수정한 레이아웃은 보존'],
|
|
'overwrite',
|
|
);
|
|
$globalStrategy = $answer;
|
|
}
|
|
|
|
// 확장별 전략 오버라이드
|
|
$strategies = $this->collectPerExtensionStrategies($updates, $globalStrategy, $force);
|
|
|
|
// 검색 인덱스 재생성 여부 — 인덱스 잠금·재색인 비용이 있어 운영 중인 사이트에서는
|
|
// 기본값(아니오)을 유지하고 유지보수 시간에 별도로 수행하는 것이 안전하다.
|
|
$rebuildSearchIndex = $this->askRebuildSearchIndex($force);
|
|
|
|
// 일괄 실행
|
|
return $this->executeBulkUpdate(
|
|
$moduleManager,
|
|
$pluginManager,
|
|
$templateManager,
|
|
$updates,
|
|
$strategies,
|
|
$rebuildSearchIndex,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 확장별 전략 오버라이드 수집.
|
|
*
|
|
* @param array $updates collectBundledUpdates() 반환값
|
|
* @return array<string, string> key: "{type}:{identifier}", value: strategy
|
|
*/
|
|
private function collectPerExtensionStrategies(array $updates, string $globalStrategy, bool $force): array
|
|
{
|
|
// lang_packs 는 layout 이 없어 overwrite/keep strategy 가 의미 없으므로 의도적으로 제외.
|
|
// 매니페스트에는 strategy 없이 그대로 전달되어 ExecuteBundledUpdatesCommand 가 일괄 처리.
|
|
$strategies = [];
|
|
foreach (['modules', 'plugins', 'templates'] as $type) {
|
|
foreach ($updates[$type] as $ext) {
|
|
$strategies["{$type}:{$ext['identifier']}"] = $globalStrategy;
|
|
}
|
|
}
|
|
|
|
if ($force) {
|
|
return $strategies;
|
|
}
|
|
|
|
if (! $this->unifiedConfirm('전역 전략과 다르게 적용할 확장이 있습니까?', false)) {
|
|
return $strategies;
|
|
}
|
|
|
|
$choices = [];
|
|
foreach ($updates['modules'] as $m) {
|
|
$choices[] = "[모듈] {$m['identifier']}";
|
|
}
|
|
foreach ($updates['plugins'] as $p) {
|
|
$choices[] = "[플러그인] {$p['identifier']}";
|
|
}
|
|
foreach ($updates['templates'] as $t) {
|
|
$choices[] = "[템플릿] {$t['identifier']}";
|
|
}
|
|
|
|
$overrideStrategy = $globalStrategy === 'overwrite' ? 'keep' : 'overwrite';
|
|
|
|
$this->info("예외 확장은 '{$overrideStrategy}' 전략이 적용됩니다.");
|
|
$selected = $this->ask('예외 확장 선택 (쉼표로 구분, 빈 값이면 건너뛰기)', '');
|
|
|
|
if (trim((string) $selected) === '') {
|
|
return $strategies;
|
|
}
|
|
|
|
$lookup = [];
|
|
$idx = 0;
|
|
foreach (['modules', 'plugins', 'templates'] as $type) {
|
|
foreach ($updates[$type] as $ext) {
|
|
$lookup[$choices[$idx]] = "{$type}:{$ext['identifier']}";
|
|
$idx++;
|
|
}
|
|
}
|
|
|
|
foreach (array_map('trim', explode(',', (string) $selected)) as $choice) {
|
|
if (isset($lookup[$choice])) {
|
|
$strategies[$lookup[$choice]] = $overrideStrategy;
|
|
}
|
|
}
|
|
|
|
return $strategies;
|
|
}
|
|
|
|
/**
|
|
* 실제 일괄 업데이트 실행.
|
|
*
|
|
* 구조 fix (beta.4 도입): 부모(`core:update`) 프로세스의 stale memory 가 신버전 sync
|
|
* 메서드를 호출하지 못하던 결함의 영구 차단. 사용자 선택을 매니페스트로 직렬화한 후
|
|
* `core:execute-bundled-updates` 를 별도 PHP 프로세스에서 spawn 하여 실행한다 (자식은
|
|
* 디스크의 fresh 코어 코드 로드).
|
|
*
|
|
* proc_open 미지원 / 실패 환경에서는 in-process fallback 으로 안전하게 전환 (기존 흐름).
|
|
*
|
|
* @return array{success: int, failed: int, skipped: int, has_updates: bool}
|
|
*/
|
|
private function executeBulkUpdate(
|
|
ModuleManager $moduleManager,
|
|
PluginManager $pluginManager,
|
|
TemplateManager $templateManager,
|
|
array $updates,
|
|
array $strategies,
|
|
bool $rebuildSearchIndex = false,
|
|
): array {
|
|
$manifest = $this->buildBundledUpdateManifest($updates, $strategies);
|
|
// 운영자의 선택을 매니페스트에 실어 spawn 자식과 in-process fallback 이 같은 결정을 따르게 한다
|
|
$manifest['rebuild_search_index'] = $rebuildSearchIndex;
|
|
if ($this->bundledManifestIsEmpty($manifest)) {
|
|
return ['success' => 0, 'failed' => 0, 'skipped' => 0, 'has_updates' => false];
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info('── 일괄 업데이트 실행 ──');
|
|
|
|
$spawnResult = $this->spawnBundledUpdates($manifest);
|
|
if ($spawnResult !== null) {
|
|
return [
|
|
'success' => $spawnResult['success'],
|
|
'failed' => $spawnResult['failed'],
|
|
'skipped' => 0,
|
|
'has_updates' => true,
|
|
];
|
|
}
|
|
|
|
// proc_open 미지원 / spawn 실패 — in-process fallback
|
|
$this->warn('별도 프로세스 spawn 실패 — in-process fallback 으로 전환합니다.');
|
|
|
|
return $this->executeBulkUpdateInProcess(
|
|
$moduleManager,
|
|
$pluginManager,
|
|
$templateManager,
|
|
$updates,
|
|
$strategies,
|
|
$rebuildSearchIndex,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 사용자 선택을 spawn 자식에 전달할 매니페스트 형식으로 직렬화.
|
|
*
|
|
* @return array{modules: array, plugins: array, templates: array, lang_packs: array}
|
|
*/
|
|
private function buildBundledUpdateManifest(array $updates, array $strategies): array
|
|
{
|
|
$manifest = [
|
|
'modules' => [],
|
|
'plugins' => [],
|
|
'templates' => [],
|
|
'lang_packs' => [],
|
|
];
|
|
|
|
foreach ($updates['modules'] ?? [] as $m) {
|
|
$id = $m['identifier'];
|
|
$manifest['modules'][] = [
|
|
'identifier' => $id,
|
|
'strategy' => $strategies["modules:{$id}"] ?? 'overwrite',
|
|
];
|
|
}
|
|
foreach ($updates['plugins'] ?? [] as $p) {
|
|
$id = $p['identifier'];
|
|
$manifest['plugins'][] = [
|
|
'identifier' => $id,
|
|
'strategy' => $strategies["plugins:{$id}"] ?? 'overwrite',
|
|
];
|
|
}
|
|
foreach ($updates['templates'] ?? [] as $t) {
|
|
$id = $t['identifier'];
|
|
$manifest['templates'][] = [
|
|
'identifier' => $id,
|
|
'strategy' => $strategies["templates:{$id}"] ?? 'overwrite',
|
|
];
|
|
}
|
|
foreach ($updates['lang_packs'] ?? [] as $lp) {
|
|
$manifest['lang_packs'][] = ['identifier' => $lp['identifier']];
|
|
}
|
|
|
|
return $manifest;
|
|
}
|
|
|
|
private function bundledManifestIsEmpty(array $manifest): bool
|
|
{
|
|
return empty($manifest['modules'])
|
|
&& empty($manifest['plugins'])
|
|
&& empty($manifest['templates'])
|
|
&& empty($manifest['lang_packs']);
|
|
}
|
|
|
|
/**
|
|
* `core:execute-bundled-updates` 를 별도 PHP 프로세스에서 실행.
|
|
*
|
|
* 자식 stdout 을 부모 콘솔로 실시간 전달 (단 `[BUNDLED-RESULT]` prefix 라인은
|
|
* 결과 페이로드로 보관 후 부모 콘솔로 노출하지 않음). 종료 후 페이로드 파싱.
|
|
*
|
|
* @return array{success: int, failed: int}|null spawn 성공 시 결과, 실패/미지원 시 null
|
|
*/
|
|
private function spawnBundledUpdates(array $manifest): ?array
|
|
{
|
|
if (! function_exists('proc_open')) {
|
|
return null;
|
|
}
|
|
|
|
$manifestPath = storage_path('app/core_pending'.DIRECTORY_SEPARATOR.'bundled-updates-manifest_'.uniqid().'.json');
|
|
File::ensureDirectoryExists(dirname($manifestPath));
|
|
File::put($manifestPath, json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
|
|
|
try {
|
|
$phpBinary = config('process.php_binary', PHP_BINARY);
|
|
$artisan = base_path('artisan');
|
|
|
|
$command = [
|
|
$phpBinary,
|
|
$artisan,
|
|
'core:execute-bundled-updates',
|
|
'--manifest='.$manifestPath,
|
|
];
|
|
$commandLine = implode(' ', array_map('escapeshellarg', $command)).' 2>&1';
|
|
|
|
$descriptors = [
|
|
0 => ['pipe', 'r'],
|
|
1 => ['pipe', 'w'],
|
|
2 => ['pipe', 'w'],
|
|
];
|
|
|
|
// ENV 합집합 (G7_UPDATE_IN_PROGRESS 등 핵심 플래그 자식에 전달)
|
|
$env = array_merge(getenv(), $_ENV);
|
|
|
|
// audit:allow spawn-after-vendor-swap-clears-package-manifest 이 spawn 은 vendor 를 바꾸지 않고,
|
|
// 두 호출처가 모두 clearAllCaches() 뒤다 — CoreUpdateCommand Step 11(패키지 매니페스트 재생성)
|
|
// 다음의 Step 12, ExecuteUpgradeStepsCommand 단독 실행의 캐시 정리 다음 단계. 따라서 자식이
|
|
// 읽는 매니페스트는 이미 현재 vendor 기준으로 다시 만들어진 것이다.
|
|
$process = proc_open($commandLine, $descriptors, $pipes, base_path(), $env);
|
|
if (! is_resource($process)) {
|
|
return null;
|
|
}
|
|
|
|
fclose($pipes[0]);
|
|
|
|
$resultPayload = null;
|
|
$resultPrefix = ExecuteBundledUpdatesCommand::RESULT_PREFIX;
|
|
|
|
while (! feof($pipes[1])) {
|
|
$line = fgets($pipes[1]);
|
|
if ($line === false) {
|
|
continue;
|
|
}
|
|
$trimmed = rtrim($line);
|
|
|
|
if (str_starts_with($trimmed, $resultPrefix)) {
|
|
$json = substr($trimmed, strlen($resultPrefix));
|
|
$decoded = json_decode($json, true);
|
|
if (is_array($decoded)) {
|
|
$resultPayload = $decoded;
|
|
}
|
|
|
|
continue; // 부모 콘솔에 노출 안 함
|
|
}
|
|
|
|
$this->line($trimmed);
|
|
}
|
|
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
proc_close($process);
|
|
|
|
if ($resultPayload === null) {
|
|
Log::warning('번들 spawn 자식이 결과 페이로드를 출력하지 않음 — 카운트 0 으로 처리');
|
|
|
|
return ['success' => 0, 'failed' => 0];
|
|
}
|
|
|
|
return [
|
|
'success' => (int) ($resultPayload['success'] ?? 0),
|
|
'failed' => (int) ($resultPayload['failed'] ?? 0),
|
|
];
|
|
} finally {
|
|
if (File::exists($manifestPath)) {
|
|
File::delete($manifestPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* in-process fallback — proc_open 미지원 환경 전용.
|
|
*
|
|
* 기존 (beta.3) 흐름의 직접 호출 패턴 보존. 단 부모 메모리의 stale 코드로 인해
|
|
* sync 메서드 누락 결함이 재현될 수 있으므로 spawn 가능 환경에서는 사용되지 않는다.
|
|
*
|
|
* @return array{success: int, failed: int, skipped: int, has_updates: bool}
|
|
*/
|
|
private function executeBulkUpdateInProcess(
|
|
ModuleManager $moduleManager,
|
|
PluginManager $pluginManager,
|
|
TemplateManager $templateManager,
|
|
array $updates,
|
|
array $strategies,
|
|
bool $rebuildSearchIndex = false,
|
|
): array {
|
|
$success = 0;
|
|
$failed = 0;
|
|
|
|
foreach ($updates['modules'] as $m) {
|
|
$id = $m['identifier'];
|
|
$strategy = $strategies["modules:{$id}"] ?? 'overwrite';
|
|
$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('번들 일괄 업데이트 실패', ['type' => 'module', 'id' => $id, 'error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
foreach ($updates['plugins'] as $p) {
|
|
$id = $p['identifier'];
|
|
$strategy = $strategies["plugins:{$id}"] ?? 'overwrite';
|
|
$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('번들 일괄 업데이트 실패', ['type' => 'plugin', 'id' => $id, 'error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
foreach ($updates['templates'] as $t) {
|
|
$id = $t['identifier'];
|
|
$strategy = $strategies["templates:{$id}"] ?? 'overwrite';
|
|
$this->line("→ [템플릿] {$id} ({$strategy})");
|
|
try {
|
|
$templateManager->updateTemplate($id, true, null, $strategy, 'bundled');
|
|
$success++;
|
|
} catch (\Throwable $e) {
|
|
$failed++;
|
|
$this->warn(" 실패: {$e->getMessage()}");
|
|
Log::error('번들 일괄 업데이트 실패', ['type' => 'template', 'id' => $id, 'error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
$langPackService = app(LanguagePackService::class);
|
|
foreach ($updates['lang_packs'] ?? [] as $lp) {
|
|
$id = $lp['identifier'];
|
|
$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('번들 일괄 업데이트 실패', ['type' => 'lang_pack', 'id' => $id, 'error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info("업데이트 완료: 성공 {$success}, 실패 {$failed}");
|
|
|
|
$this->runSearchIndexRebuildIfRequested($rebuildSearchIndex);
|
|
|
|
return [
|
|
'success' => $success,
|
|
'failed' => $failed,
|
|
'skipped' => 0,
|
|
'has_updates' => true,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 검색 인덱스 재생성 여부를 운영자에게 확인합니다.
|
|
*
|
|
* 재생성은 인덱스 잠금 또는 전체 재색인을 유발하므로 기본값은 "아니오" 입니다.
|
|
* `--force` (무인 실행) 시에도 묻지 않고 수행하지 않습니다 — 무인 실행이
|
|
* 대용량 테이블을 잠그는 일이 없어야 합니다. 필요하면 `core:update
|
|
* --rebuild-search-index` 로 명시하거나 이후 `search:index --repair` 를 실행합니다.
|
|
*
|
|
* @param bool $force 강제(무인) 실행 여부
|
|
* @return bool 재생성 수행 여부
|
|
*/
|
|
private function askRebuildSearchIndex(bool $force): bool
|
|
{
|
|
// 커맨드가 옵션으로 이미 선택을 받았으면 그대로 따른다
|
|
if ($this->hasOption('rebuild-search-index') && (bool) $this->option('rebuild-search-index')) {
|
|
return true;
|
|
}
|
|
|
|
if ($force) {
|
|
return false;
|
|
}
|
|
|
|
$manager = app(SearchIndexMaintenanceManager::class);
|
|
|
|
// 점검을 제공하지 않는 엔진에서는 물을 것이 없다
|
|
if (! $manager->hasMaintainer() || $manager->unavailableReason() !== null) {
|
|
return false;
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->line(' '.__('search.index.rebuild_cost_warning'));
|
|
|
|
return $this->unifiedConfirm(__('search.index.rebuild_after_bulk_confirm'), false);
|
|
}
|
|
|
|
/**
|
|
* 요청이 있었으면 검색 인덱스를 재생성합니다.
|
|
*
|
|
* @param bool $requested 재생성 요청 여부
|
|
* @return void
|
|
*/
|
|
private function runSearchIndexRebuildIfRequested(bool $requested): void
|
|
{
|
|
if (! $requested) {
|
|
return;
|
|
}
|
|
|
|
$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]));
|
|
}
|
|
}
|
|
}
|