이슈 후속(S7) 마감 + 릴리스 버전 확정. 기능(S7): - 사이트맵 전체 재생성 완료/실패 시 실행 관리자에게 알림(수동 실행 한정). 스케줄러·증분·봇 재생성은 알림 없음(triggered_by 부재 시 skip). - 최종 실패 전용 알림 + 재생성 전용 큐 + WS 진행상황 payload 봉투 정정. - Reverb OFF 폴링 폴백 경로 수정(실브라우저 폴링에서만 드러난 결함). 버전(D28 — 정정, D20 번복): - 릴리스를 7.0.5→7.0.6(patch)로 확정. 7.1.0(minor)은 다른 브랜치에서 별도 진행. - config/app.php + .env + README + INSTALL + CHANGELOG + 모듈 3 requires 를 7.0.6 로. - D19 SEO 캐시 이행 업그레이드 스텝을 Upgrade_7_0_6 로 리네임. 이유: 스텝 버전 게이팅이 version<=toVersion 이라, 7.1.0 이름이면 7.0.5→7.0.6 업그레이드에서 이행이 실행되지 않는다. 검증: composer test-smoke OK(2·29) · 재생성 알림/업그레이드 이행/진행상황 테스트 green · version-sync/version-bump(new-bump)/extension-require green · audit 0 error.
187 lines
7.1 KiB
PHP
187 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Upgrades\Data\V7_0_6\Migrations;
|
|
|
|
use App\Extension\Upgrade\DataMigration;
|
|
use App\Extension\UpgradeContext;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
/**
|
|
* SEO 캐시 설정을 "미설정(null) = 고급 탭 값 사용" 의미로 이행 (결정 D19).
|
|
*
|
|
* 배경:
|
|
* 캐시 설정은 `cache.*`(고급 탭)로 이관되었으나 SEO 탭의 옛 칸(`seo.cache_enabled`,
|
|
* `seo.cache_ttl`, `seo.sitemap_cache_ttl`)이 제거되지 않았고, 코드가
|
|
* `g7_core_settings('cache.X', g7_core_settings('seo.Y', 기본값))` 형태로 cache 를
|
|
* **항상** 우선시켜 SEO 탭 입력이 조용히 무시되었다(운영자가 저장해도 무효).
|
|
*
|
|
* 7.0.6 의 새 의미:
|
|
* 고급 탭 = 메인, SEO 탭에 **별도 지정이 있으면** 그것이 오버라이드.
|
|
* "별도 지정" 판정은 null 여부로 하므로 미설정 상태를 표현할 수 있어야 한다.
|
|
*
|
|
* 본 마이그레이션의 동작 (옛 기본값 기준 추정):
|
|
*
|
|
* | 저장된 seo.* 값 | 판정 | 동작 |
|
|
* |------------------|-----------------------|--------------------------|
|
|
* | 키 부재 | 미설정 | no-op |
|
|
* | 이미 null | 미설정 | no-op (멱등) |
|
|
* | 옛 기본값과 동일 | 안 건드린 것으로 추정 | null 로 비움 |
|
|
* | 옛 기본값과 다름 | 의도적으로 바꾼 것 | 보존 → 오버라이드로 발동 |
|
|
*
|
|
* 즉 값을 실제로 바꿔둔 운영자의 의도만 살아난다. 그 결과 **업그레이드 후 동작이
|
|
* 바뀔 수 있다** — 예: `seo.cache_ttl=3600` 을 설정해 둔 사이트는 그동안 무시된 채
|
|
* `cache.seo_ttl`(7200)로 돌았으나 이제 3600 이 발동한다. 이것이 원래 의도였으므로
|
|
* 의도된 변경이며 CHANGELOG 에 고지한다.
|
|
*
|
|
* 멱등: 재실행 시 이미 null 인 키는 건드리지 않는다.
|
|
*
|
|
* 실패 정책: 설정 정리 실패가 업그레이드 전체를 막지 않는다(경고 후 계속). 비우지 못하면
|
|
* 해당 키는 옛 기본값을 유지한 채 오버라이드로 동작하는데, 그 값이 곧 고급 탭 기본값과
|
|
* 같으므로 관측되는 동작은 변하지 않는다.
|
|
*
|
|
* 격리 원칙 (docs/extension/upgrade-step-guide.md §13):
|
|
* `SeoCacheSettings` 등 신규 코어 클래스를 참조하지 않는다. 옛 기본값 목록과 설정
|
|
* 읽기/쓰기를 본 클래스 안에 로컬로 인라인 구현해 V-1 안전을 확보한다.
|
|
*/
|
|
final class NullifyUntouchedSeoCacheOverrides implements DataMigration
|
|
{
|
|
/**
|
|
* 설정 카테고리 파일 (settings 디스크 기준 상대 경로)
|
|
*/
|
|
private const SEO_SETTINGS_FILE = 'seo.json';
|
|
|
|
/**
|
|
* 7.0.x 시점의 seo 카테고리 캐시 키 기본값.
|
|
*
|
|
* 저장된 값이 이 값과 같으면 "운영자가 안 건드린 것" 으로 추정해 null 로 비운다.
|
|
* 격리를 위해 defaults.json 을 읽지 않고 로컬 상수로 고정한다 — 미래 버전에서
|
|
* defaults.json 이 바뀌어도 본 스텝의 판정 기준은 동결되어야 한다.
|
|
*/
|
|
private const LEGACY_DEFAULTS = [
|
|
'cache_enabled' => true,
|
|
'cache_ttl' => 7200,
|
|
'sitemap_cache_ttl' => 86400,
|
|
];
|
|
|
|
/**
|
|
* 마이그레이션 식별자 (로그용).
|
|
*
|
|
* @return string 식별자
|
|
*/
|
|
public function name(): string
|
|
{
|
|
return 'NullifyUntouchedSeoCacheOverrides';
|
|
}
|
|
|
|
/**
|
|
* 옛 기본값 그대로인 SEO 캐시 키를 null(미설정)로 비웁니다.
|
|
*
|
|
* @param UpgradeContext $context 업그레이드 컨텍스트
|
|
*/
|
|
public function run(UpgradeContext $context): void
|
|
{
|
|
try {
|
|
$this->runInternal($context);
|
|
} catch (\Throwable $e) {
|
|
// 업그레이드 중단 금지 — 설정 정리 실패가 업그레이드 전체를 막지 않는다.
|
|
$context->logger->warning(sprintf(
|
|
'[7.0.6] NullifyUntouchedSeoCacheOverrides 실패 (seo.json 보존, 계속 진행): %s',
|
|
$e->getMessage(),
|
|
));
|
|
Log::warning('7.0.6 NullifyUntouchedSeoCacheOverrides 실패', [
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 실제 이행 로직.
|
|
*
|
|
* @param UpgradeContext $context 업그레이드 컨텍스트
|
|
*/
|
|
private function runInternal(UpgradeContext $context): void
|
|
{
|
|
$settings = $this->readSeoSettings($context);
|
|
if ($settings === null) {
|
|
return;
|
|
}
|
|
|
|
$cleared = [];
|
|
$kept = [];
|
|
|
|
foreach (self::LEGACY_DEFAULTS as $key => $legacyDefault) {
|
|
if (! array_key_exists($key, $settings) || $settings[$key] === null) {
|
|
continue;
|
|
}
|
|
|
|
// 옛 기본값과 완전히 같을 때만 "안 건드림" 으로 본다 (타입까지 일치 요구)
|
|
if ($settings[$key] === $legacyDefault) {
|
|
$settings[$key] = null;
|
|
$cleared[] = $key;
|
|
|
|
continue;
|
|
}
|
|
|
|
$kept[$key] = $settings[$key];
|
|
}
|
|
|
|
foreach ($kept as $key => $value) {
|
|
$context->logger->info(sprintf(
|
|
'[7.0.6] seo.%s = %s 는 옛 기본값과 달라 오버라이드로 보존 — 이제부터 실제 적용됩니다',
|
|
$key,
|
|
var_export($value, true),
|
|
));
|
|
}
|
|
|
|
if ($cleared === []) {
|
|
$context->logger->info('[7.0.6] SEO 캐시 설정 — 비울 대상 없음 (이미 이행 완료이거나 전부 오버라이드)');
|
|
|
|
return;
|
|
}
|
|
|
|
$this->writeSeoSettings($settings);
|
|
$context->logger->info('[7.0.6] SEO 캐시 설정 — 미설정(null)으로 비운 키: '.implode(', ', $cleared));
|
|
}
|
|
|
|
/**
|
|
* seo 설정 카테고리를 읽습니다.
|
|
*
|
|
* @param UpgradeContext $context 업그레이드 컨텍스트
|
|
* @return array<string, mixed>|null 설정 배열 (파일 부재/손상 시 null)
|
|
*/
|
|
private function readSeoSettings(UpgradeContext $context): ?array
|
|
{
|
|
if (! Storage::disk('settings')->exists(self::SEO_SETTINGS_FILE)) {
|
|
$context->logger->info('[7.0.6] seo.json 부재 — 정리 대상 없음, skip');
|
|
|
|
return null;
|
|
}
|
|
|
|
$decoded = json_decode((string) Storage::disk('settings')->get(self::SEO_SETTINGS_FILE), true);
|
|
|
|
if (! is_array($decoded)) {
|
|
// 손상 파일을 덮어쓰지 않는다 (파괴 금지)
|
|
$context->logger->warning('[7.0.6] seo.json 파싱 실패 — 파일 보존하고 skip');
|
|
|
|
return null;
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
/**
|
|
* seo 설정 카테고리를 저장합니다.
|
|
*
|
|
* @param array<string, mixed> $settings 저장할 설정 배열
|
|
*/
|
|
private function writeSeoSettings(array $settings): void
|
|
{
|
|
Storage::disk('settings')->put(
|
|
self::SEO_SETTINGS_FILE,
|
|
json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
|
|
);
|
|
}
|
|
}
|